Skip to content

Commit

Permalink
feat: construction derive staking support (#207)
Browse files Browse the repository at this point in the history
* feat: add staking_credential and address_type in ConstructionDeriveRequest

* refactor: move staking_credential and address_type into metadata of ConstructionDeriveRequest

* feat: add invalid address type error

* feat: add metadata validation in construction derive controller

* refactor: generatePayload parameters in construction derive tests

* test: construction derive with invalid staking key

* refactor: exclude enum for address type in openApi

* test: construction derive with invalid address type

* test: construction derive for Base and Reward addresses

* feat: construction derive Base and Reward address support

* test: construction derive validation for staking key size

* test: construction derive reward address correct bech32 address

* fix: correct address prefix for reward address creation

* refactor: move UTxOAddressTypes enum to constants file

* refactor: rename UTxOAddressType and make Enterprise default value

* refactor: add comment for staking address prefix

* test: add comments to construction derive tests

* refactor: use proper error for invalid staking key

* test: construction derive error when no staking key for base address

* feat: missing staking key validation for base address generation

* refactor: rename AddressType

* refactor: address type description in openApi

Refs #198
  • Loading branch information
tomasBustamante authored and AlanVerbner committed Dec 8, 2020
1 parent 2240d9e commit 0d8250a
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { ErrorFactory } from '../utils/errors';
import { withNetworkValidation } from './controllers-helper';
import { CardanoCli } from '../utils/cardanonode-cli';
import { AddressType } from '../utils/constants';

export interface ConstructionController {
constructionDerive(
Expand Down Expand Up @@ -50,6 +51,8 @@ export interface ConstructionController {
const isKeyValid = (publicKeyBytes: string, curveType: string): boolean =>
publicKeyBytes.length === PUBLIC_KEY_BYTES_LENGTH && curveType === 'edwards25519';

const isAddressTypeValid = (type: string): boolean => ['Enterprise', 'Base', 'Reward', '', undefined].includes(type);

const configure = (
constructionService: ConstructionService,
cardanoService: CardanoService,
Expand All @@ -72,8 +75,37 @@ const configure = (
}
logger.info('[constructionDerive] Public key has a valid format');

// eslint-disable-next-line camelcase
const stakingCredential = request.body.metadata?.staking_credential;
if (stakingCredential) {
logger.info('[constructionDerive] About to check if staking credential has valid length and curve type');
if (!isKeyValid(stakingCredential.hex_bytes, stakingCredential.curve_type)) {
logger.info('[constructionDerive] Staking credential has an invalid format');
throw ErrorFactory.invalidStakingKeyFormat();
}
logger.info('[constructionDerive] Staking credential key has a valid format');
}

// eslint-disable-next-line camelcase
const addressType = request.body.metadata?.address_type;
if (addressType) {
logger.info('[constructionDerive] About to check if address type is valid');
if (!isAddressTypeValid(addressType)) {
logger.info('[constructionDerive] Address type has an invalid value');
throw ErrorFactory.invalidAddressTypeError();
}
logger.info('[constructionDerive] Address type has a valid value');
}

logger.info(request.body, '[constructionDerive] About to generate address');
const address = cardanoService.generateAddress(logger, networkIdentifier, publicKey.hex_bytes);
const address = cardanoService.generateAddress(
logger,
networkIdentifier,
publicKey.hex_bytes,
// eslint-disable-next-line camelcase
stakingCredential?.hex_bytes,
addressType as AddressType
);
if (!address) {
logger.error('[constructionDerive] There was an error generating address');
throw ErrorFactory.addressGenerationError();
Expand Down
14 changes: 13 additions & 1 deletion cardano-rosetta-server/src/server/openApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,10 @@
"minimum": 0,
"example": 1582833600000
},
"AddressType": {
"description": "* Base address - associated to a payment and a staking credential, * Reward address - associated to a staking credential * Enterprise address - holds no delegation rights and will be created when no stake key is sent to the API",
"type": "string"
},
"PublicKey": {
"description": "PublicKey contains a public key byte array for a particular CurveType encoded in hex. Note that there is no PrivateKey struct as this is NEVER the concern of an implementation.",
"type": "object",
Expand Down Expand Up @@ -1465,7 +1469,15 @@
"$ref": "#/components/schemas/PublicKey"
},
"metadata": {
"type": "object"
"type": "object",
"properties": {
"staking_credential": {
"$ref": "#/components/schemas/PublicKey"
},
"address_type": {
"$ref": "#/components/schemas/AddressType"
}
}
}
}
},
Expand Down
84 changes: 64 additions & 20 deletions cardano-rosetta-server/src/server/services/cardano-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cbor from 'cbor';
import { Logger } from 'fastify';
import { ErrorFactory } from '../utils/errors';
import { hexFormatter } from '../utils/formatters';
import { ADA, ADA_DECIMALS, operationType } from '../utils/constants';
import { ADA, ADA_DECIMALS, operationType, AddressType } from '../utils/constants';

// Nibbles
export const SIGNATURE_LENGTH = 128;
Expand Down Expand Up @@ -35,7 +35,7 @@ export interface LinearFeeParameters {
minFeeB: number;
}

export enum AddressType {
export enum EraAddressType {
Shelley,
Byron
}
Expand All @@ -49,15 +49,23 @@ export interface CardanoService {
*
* @param networkId cardano network
* @param publicKey public key hex string representation
* @param stakingCredential hex string representation
* @param type Address type: either Enterprise, Base or Reward
*/
generateAddress(logger: Logger, networkId: NetworkIdentifier, publicKey: string): string | null;
generateAddress(
logger: Logger,
networkId: NetworkIdentifier,
publicKey: string,
stakingCredential?: string,
type?: AddressType
): string | null;

/**
* This function returns the address type based on a string encoded one
* Returns the era address type (either Shelley or Byron) based on an encoded string
*
* @param address to be parsed
*/
getAddressType(address: string): AddressType | null;
getEraAddressType(address: string): EraAddressType | null;

/**
* Returns the transaction hash for the given signed transaction.
Expand Down Expand Up @@ -153,6 +161,10 @@ const calculateFee = (inputs: Components.Schemas.Operation[], outputs: Component
const getAddressPrefix = (network: number) =>
network === NetworkIdentifier.CARDANO_MAINNET_NETWORK ? 'addr' : 'addr_test';

// Prefix according to: https://github.com/cardano-foundation/CIPs/tree/master/CIP5#specification
const getStakeAddressPrefix = (network: number) =>
network === NetworkIdentifier.CARDANO_MAINNET_NETWORK ? 'stake' : 'stake_test';

const parseInputToOperation = (input: CardanoWasm.TransactionInput, index: number): Components.Schemas.Operation => ({
operation_identifier: { index },
coin_change: {
Expand Down Expand Up @@ -308,7 +320,7 @@ const getWitnessesForTransaction = (logger: Logger, signatures: Signatures[]): C
const getUniqueAddresses = (addresses: string[]) => [...new Set(addresses)];

const configure = (linearFeeParameters: LinearFeeParameters): CardanoService => ({
generateAddress(logger, network, publicKey) {
generateAddress(logger, network, publicKey, stakingCredential, type = AddressType.ENTERPRISE) {
logger.info(
`[generateAddress] About to generate address from public key ${publicKey} and network identifier ${network}`
);
Expand All @@ -317,23 +329,55 @@ const configure = (linearFeeParameters: LinearFeeParameters): CardanoService =>

const pub = CardanoWasm.PublicKey.from_bytes(publicKeyBuffer);

logger.info('[generateAddress] Deriving cardano address from valid public key');
const enterpriseAddress = CardanoWasm.EnterpriseAddress.new(
network,
CardanoWasm.StakeCredential.from_keyhash(pub.hash())
);
const address = enterpriseAddress.to_address().to_bech32(getAddressPrefix(network));
logger.info(`[generateAddress] base address is ${address}`);
return address;
const payment = CardanoWasm.StakeCredential.from_keyhash(pub.hash());

if (type === AddressType.REWARD) {
logger.info('[generateAddress] Deriving cardano enterprise address from valid public staking key');
const rewardAddress = CardanoWasm.RewardAddress.new(network, payment);
const bech32address = rewardAddress.to_address().to_bech32(getStakeAddressPrefix(network));
logger.info(`[generateAddress] reward address is ${bech32address}`);
return bech32address;
}

if (type === AddressType.BASE) {
if (!stakingCredential) {
logger.error('[constructionDerive] No staking key was provided for base address creation');
throw ErrorFactory.missingStakingKeyError();
}
const stakingKeyBuffer = Buffer.from(stakingCredential, 'hex');

const staking = CardanoWasm.PublicKey.from_bytes(stakingKeyBuffer);

logger.info('[generateAddress] Deriving cardano address from valid public key and staking key');
const baseAddress = CardanoWasm.BaseAddress.new(
network,
payment,
CardanoWasm.StakeCredential.from_keyhash(staking.hash())
);
const bech32address = baseAddress.to_address().to_bech32(getAddressPrefix(network));
logger.info(`[generateAddress] base address is ${bech32address}`);
return bech32address;
}

if (type === AddressType.ENTERPRISE) {
logger.info('[generateAddress] Deriving cardano enterprise address from valid public key');
const enterpriseAddress = CardanoWasm.EnterpriseAddress.new(network, payment);
const bech32address = enterpriseAddress.to_address().to_bech32(getAddressPrefix(network));
logger.info(`[generateAddress] enterprise address is ${bech32address}`);
return bech32address;
}

logger.info('[generateAddress] Address type has an invalid value');
throw ErrorFactory.invalidAddressTypeError();
},

getAddressType(address) {
getEraAddressType(address) {
if (CardanoWasm.ByronAddress.is_valid(address)) {
return AddressType.Byron;
return EraAddressType.Byron;
}
try {
CardanoWasm.Address.from_bech32(address);
return AddressType.Shelley;
return EraAddressType.Shelley;
} catch (error) {
return null;
}
Expand Down Expand Up @@ -412,13 +456,13 @@ const configure = (linearFeeParameters: LinearFeeParameters): CardanoService =>
const { bytes, addresses } = this.createUnsignedTransaction(logger, operations, ttl);
// eslint-disable-next-line consistent-return
const signatures: Signatures[] = getUniqueAddresses(addresses).map(address => {
switch (this.getAddressType(address)) {
case AddressType.Shelley:
switch (this.getEraAddressType(address)) {
case EraAddressType.Shelley:
return {
signature: SHELLEY_DUMMY_SIGNATURE,
publicKey: SHELLEY_DUMMY_PUBKEY
};
case AddressType.Byron: // FIXME: handle this properly when supporting byron in a separate PR
case EraAddressType.Byron: // FIXME: handle this properly when supporting byron in a separate PR
case null:
throw ErrorFactory.invalidAddressError(address);
}
Expand Down
6 changes: 6 additions & 0 deletions cardano-rosetta-server/src/server/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ export const SUCCESS_OPERATION_STATE = {
status: operationTypeStatus.SUCCESS,
successful: true
};

export enum AddressType {
ENTERPRISE = 'Enterprise',
BASE = 'Base',
REWARD = 'Reward'
}
14 changes: 13 additions & 1 deletion cardano-rosetta-server/src/server/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ export const Errors = {
message: 'Provided address is invalid',
code: 4015
},
INVALID_ADDRESS_TYPE: {
message: 'Provided address type is invalid',
code: 4016
},
INVALID_STAKING_KEY_FORMAT: { message: 'Invalid staking key format', code: 4017 },
STAKING_KEY_MISSING: { message: 'Staking key is required for this type of address', code: 4018 },
UNSPECIFIED_ERROR: { message: 'An error occurred', code: 5000 },
NOT_IMPLEMENTED: { message: 'Not implemented', code: 5001 },
ADDRESS_GENERATION_ERROR: { message: 'Address generation error', code: 5002 },
Expand Down Expand Up @@ -86,6 +92,8 @@ const genesisBlockNotFound: CreateErrorFunction = () => buildApiError(Errors.GEN
const transactionNotFound: CreateErrorFunction = () => buildApiError(Errors.TRANSACTION_NOT_FOUND, false);
const addressGenerationError: CreateErrorFunction = () => buildApiError(Errors.ADDRESS_GENERATION_ERROR, false);
const invalidPublicKeyFormat: CreateErrorFunction = () => buildApiError(Errors.INVALID_PUBLIC_KEY_FORMAT, false);
const invalidStakingKeyFormat: CreateErrorFunction = () => buildApiError(Errors.INVALID_STAKING_KEY_FORMAT, false);
const missingStakingKeyError: CreateErrorFunction = type => buildApiError(Errors.STAKING_KEY_MISSING, false, type);
const parseSignedTransactionError: CreateErrorFunction = () =>
buildApiError(Errors.PARSE_SIGNED_TRANSACTION_ERROR, false);
const cantBuildWitnessesSet: CreateErrorFunction = () => buildApiError(Errors.CANT_BUILD_WITNESSES_SET, false);
Expand All @@ -107,6 +115,7 @@ const transactionInputDeserializationError: CreateErrorFunction = (details?: str
const transactionOutputDeserializationError: CreateErrorFunction = (details?: string) =>
buildApiError(Errors.TRANSACTION_OUTPUT_DESERIALIZATION_ERROR, false, details);
const invalidAddressError: CreateErrorFunction = address => buildApiError(Errors.INVALID_ADDRESS, true, address);
const invalidAddressTypeError: CreateErrorFunction = type => buildApiError(Errors.INVALID_ADDRESS_TYPE, true, type);

export const ErrorFactory = {
blockNotFoundError,
Expand All @@ -119,6 +128,8 @@ export const ErrorFactory = {
transactionNotFound,
addressGenerationError,
invalidPublicKeyFormat,
invalidStakingKeyFormat,
missingStakingKeyError,
parseSignedTransactionError,
cantBuildSignedTransaction,
cantBuildWitnessesSet,
Expand All @@ -130,7 +141,8 @@ export const ErrorFactory = {
sendTransactionError,
transactionInputDeserializationError,
transactionOutputDeserializationError,
invalidAddressError
invalidAddressError,
invalidAddressTypeError
};

export const configNotFoundError: CreateServerErrorFunction = () =>
Expand Down
9 changes: 8 additions & 1 deletion cardano-rosetta-server/src/types/rosetta-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ declare namespace Components {
*/
metadata?: {};
}
/**
* * Base address - associated to a payment and a staking credential, * Reward address - associated to a staking credential * Enterprise address - holds no delegation rights and will be created when no stake key is sent to the API
*/
export type AddressType = string;
/**
* Allow specifies supported Operation status, Operation types, and all possible error statuses. This Allow object is used by clients to validate the correctness of a Rosetta Server implementation. It is expected that these clients will error if they receive some response that contains any of the above information that is not specified here.
*/
Expand Down Expand Up @@ -212,7 +216,10 @@ declare namespace Components {
export interface ConstructionDeriveRequest {
network_identifier: /* The network_identifier specifies which network a particular object is associated with. */ NetworkIdentifier;
public_key: /* PublicKey contains a public key byte array for a particular CurveType encoded in hex. Note that there is no PrivateKey struct as this is NEVER the concern of an implementation. */ PublicKey;
metadata?: {};
metadata?: {
staking_credential?: /* PublicKey contains a public key byte array for a particular CurveType encoded in hex. Note that there is no PrivateKey struct as this is NEVER the concern of an implementation. */ PublicKey;
address_type?: /* * Base address - associated to a payment and a staking credential, * Reward address - associated to a staking credential * Enterprise address - holds no delegation rights and will be created when no stake key is sent to the API */ AddressType;
};
}
/**
* ConstructionDeriveResponse is returned by the `/construction/derive` endpoint.
Expand Down

0 comments on commit 0d8250a

Please sign in to comment.