Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/198 construction derive staking support #207

Merged
merged 22 commits into from
Nov 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
588a2e0
feat: add staking_credential and address_type in ConstructionDeriveRe…
tomasBustamante Nov 4, 2020
6a290b7
refactor: move staking_credential and address_type into metadata of C…
tomasBustamante Nov 4, 2020
18ab560
feat: add invalid address type error
tomasBustamante Nov 5, 2020
fb4c1bf
feat: add metadata validation in construction derive controller
tomasBustamante Nov 5, 2020
dd4dcc2
refactor: generatePayload parameters in construction derive tests
tomasBustamante Nov 5, 2020
53152d6
test: construction derive with invalid staking key
tomasBustamante Nov 6, 2020
d40f8b1
refactor: exclude enum for address type in openApi
tomasBustamante Nov 6, 2020
692e36e
test: construction derive with invalid address type
tomasBustamante Nov 6, 2020
bca82ad
test: construction derive for Base and Reward addresses
tomasBustamante Nov 6, 2020
4067e70
feat: construction derive Base and Reward address support
tomasBustamante Nov 6, 2020
14e1768
test: construction derive validation for staking key size
tomasBustamante Nov 6, 2020
c0acdbe
test: construction derive reward address correct bech32 address
tomasBustamante Nov 6, 2020
c00045c
fix: correct address prefix for reward address creation
tomasBustamante Nov 6, 2020
d292738
refactor: move UTxOAddressTypes enum to constants file
tomasBustamante Nov 6, 2020
71052d6
refactor: rename UTxOAddressType and make Enterprise default value
tomasBustamante Nov 9, 2020
1d59110
refactor: add comment for staking address prefix
tomasBustamante Nov 9, 2020
6bbfdbf
test: add comments to construction derive tests
tomasBustamante Nov 9, 2020
d007b34
refactor: use proper error for invalid staking key
tomasBustamante Nov 9, 2020
f545713
test: construction derive error when no staking key for base address
tomasBustamante Nov 9, 2020
e38870d
feat: missing staking key validation for base address generation
tomasBustamante Nov 9, 2020
8a1ebdb
refactor: rename AddressType
tomasBustamante Nov 10, 2020
b0f88c6
refactor: address type description in openApi
tomasBustamante Nov 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
tomasBustamante marked this conversation as resolved.
Show resolved Hide resolved
// 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(
tomasBustamante marked this conversation as resolved.
Show resolved Hide resolved
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) =>
tomasBustamante marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -307,7 +319,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 @@ -316,23 +328,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 @@ -411,13 +455,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 @@ -29,3 +29,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