Skip to content

Commit

Permalink
fix: fill parse with missing information so it can be processed by ch…
Browse files Browse the repository at this point in the history
…eck tool
  • Loading branch information
AlanVerbner authored and t-dallas committed Aug 25, 2020
1 parent 5ba3dab commit dd3dbb0
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 371 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"homepage": "https://github.com/input-output-hk/cardano-rosetta#readme",
"devDependencies": {
"@atixlabs/eslint-config": "1.2.3",
"@types/cbor": "5.0.1",
"@types/dockerode": "2.5.34",
"@types/jest": "26.0.3",
"@types/node": "14.0.14",
Expand Down Expand Up @@ -68,6 +69,7 @@
},
"dependencies": {
"@emurgo/cardano-serialization-lib-nodejs": "2.0.0",
"cbor": "5.1.0",
"dotenv": "8.2.0",
"execa": "4.0.3",
"fastify": "2.15.1",
Expand Down
Binary file added packages-cache/@types-cbor-5.0.1.tgz
Binary file not shown.
Binary file added packages-cache/bignumber.js-9.0.0.tgz
Binary file not shown.
Binary file added packages-cache/cbor-5.1.0.tgz
Binary file not shown.
Binary file added packages-cache/nofilter-1.0.4.tgz
Binary file not shown.
67 changes: 25 additions & 42 deletions src/server/services/cardano-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CardanoWasm, { BigNum, Vkey, PublicKey, Ed25519Signature } from '@emurgo/
import { Logger } from 'fastify';
import { ErrorFactory } from '../utils/errors';
import { hexFormatter } from '../utils/formatters';
import { SUCCESS_STATUS, TRANSFER_OPERATION_TYPE, ADA, ADA_DECIMALS } from '../utils/constants';
import { TRANSFER_OPERATION_TYPE, ADA, ADA_DECIMALS } from '../utils/constants';

const PUBLIC_KEY_LENGTH = 32;
const PUBLIC_KEY_BYTES_LENGTH = 64;
Expand Down Expand Up @@ -44,8 +44,16 @@ export interface CardanoService {
ttl: number
): CardanoWasm.TransactionBody;
createUnsignedTransaction(operations: Components.Schemas.Operation[], ttl: string): UnsignedTransaction;
parseSignedTransaction(networkId: NetworkIdentifier, transaction: string): TransactionParsed;
parseUnsignedTransaction(networkId: NetworkIdentifier, transaction: string): TransactionParsed;
parseSignedTransaction(
networkId: NetworkIdentifier,
transaction: string,
extraData: Components.Schemas.Operation[]
): TransactionParsed;
parseUnsignedTransaction(
networkId: NetworkIdentifier,
transaction: string,
extraData: Components.Schemas.Operation[]
): TransactionParsed;
}

const calculateFee = (inputs: Components.Schemas.Operation[], outputs: Components.Schemas.Operation[]): BigInt => {
Expand Down Expand Up @@ -96,16 +104,18 @@ const getRelatedOperationsFromInputs = (

const parseOperationsFromTransactionBody = (
transactionBody: CardanoWasm.TransactionBody,
extraData: Components.Schemas.Operation[],
network: number
): Components.Schemas.Operation[] => {
const operations = [];
const inputsCount = transactionBody.inputs().len();
const outputsCount = transactionBody.outputs().len();
let currentIndex = 0;
while (currentIndex < inputsCount) {
const input = transactionBody.inputs().get(currentIndex++);
const input = transactionBody.inputs().get(currentIndex);
const inputParsed = parseInputToOperation(input, operations.length);
operations.push(inputParsed);
operations.push({ ...inputParsed, ...extraData[currentIndex], status: '' });
currentIndex++;
}
currentIndex = 0;
// till this line operations only contains inputs
Expand All @@ -123,31 +133,6 @@ const parseOperationsFromTransactionBody = (
return operations;
};

const getSignatures = (witnessesSet: CardanoWasm.TransactionWitnessSet): string[] => {
if (!witnessesSet.vkeys()) {
return [];
}
const signatures = [];
const witnessesKeys = witnessesSet.vkeys();
const witnessesLength = witnessesKeys ? witnessesKeys.len() : 0;
let currentWitnessLength = 0;
while (witnessesKeys && currentWitnessLength < witnessesLength) {
signatures.push(
hexFormatter(
Buffer.from(
witnessesKeys
.get(currentWitnessLength++)
.vkey()
.public_key()
.hash()
.to_bytes()
)
)
);
}
return signatures;
};

const configure = (logger: Logger): CardanoService => ({
generateAddress(network, publicKey) {
logger.info(
Expand Down Expand Up @@ -326,31 +311,29 @@ const configure = (logger: Logger): CardanoService => ({
createTransactionBody(inputs, outputs, fee, ttl) {
return CardanoWasm.TransactionBody.new(inputs, outputs, BigNum.new(fee), ttl);
},
parseSignedTransaction(networkId, transaction) {
parseSignedTransaction(networkId, transaction, extraData) {
try {
const transactionBuffer = Buffer.from(transaction, 'hex');
logger.info('[parseSignedTransaction] About to create signed transaction from bytes');
const parsed = CardanoWasm.Transaction.from_bytes(transactionBuffer);
logger.info('[parseSignedTransaction] About to parse operations from transaction body');
const operations = parseOperationsFromTransactionBody(parsed.body(), networkId);
const operations = parseOperationsFromTransactionBody(parsed.body(), extraData, networkId);
logger.info('[parseSignedTransaction] About to get signatures from parsed transaction');
const signatures = getSignatures(parsed.witness_set());
logger.info(
`[parseSignedTransaction] Returning ${operations.length} operations and ${signatures.length} signers`
);
return { operations, signers: signatures };
logger.info(operations, '[parseSignedTransaction] Returning operations');
const signers = extraData.map(data => data.account?.address || '');
return { operations, signers };
} catch (error) {
logger.error({ error }, '[parseUnsignedTransaction] Cant instantiate signed transaction from transaction bytes');
logger.error({ error }, '[parseSignedTransaction] Cant instantiate signed transaction from transaction bytes');
throw ErrorFactory.cantCreateSignedTransactionFromBytes();
}
},
parseUnsignedTransaction(networkId, transaction) {
parseUnsignedTransaction(networkId, transaction, extraData) {
try {
logger.info(transaction, '[parseUnsignedTransaction] About to create unsigned transaction from bytes');
const transactionBuffer = Buffer.from(transaction, 'hex');
logger.info('[parseUnsignedTransaction] About to create unsigned transaction from bytes');
const parsed = CardanoWasm.TransactionBody.from_bytes(transactionBuffer);
logger.info('[parseUnsignedTransaction] About to parse operations from transaction body');
const operations = parseOperationsFromTransactionBody(parsed, networkId);
logger.info(extraData, '[parseUnsignedTransaction] About to parse operations from transaction body');
const operations = parseOperationsFromTransactionBody(parsed, extraData, networkId);
logger.info(operations, `[parseUnsignedTransaction] Returning ${operations.length} operations`);
return { operations, signers: [] };
} catch (error) {
Expand Down
50 changes: 43 additions & 7 deletions src/server/services/construction-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Logger } from 'pino';
import cbor from 'cbor';
import { CardanoService, NetworkIdentifier } from './cardano-services';
import { NetworkRepository } from '../db/network-repository';
import { withNetworkValidation } from './utils/services-helper';
Expand Down Expand Up @@ -57,6 +58,26 @@ const constructPayloadsForTransactionBody = (
// eslint-disable-next-line camelcase
addresses.map(address => ({ address, hex_bytes: transactionBodyHash, signature_type: SIGNATURE_TYPE }));

/**
* Rosetta Api requires some information during the workflow that's not available in an UTXO based blockchain,
* for example input amounts. Because of that we need to encode some extra data to be able to recover it, for example,
* when parsing the transaction. For further explanation see:
* https://community.rosetta-api.org/t/implementing-the-construction-api-for-utxo-model-coins/100/3
*
* CBOR is being used to follow standard Cardano serialization library
*
* @param transaction
* @param extraData
*/
// TODO: this function is going to be moved when implementing https://github.com/input-output-hk/cardano-rosetta/issues/56
const encodeExtraData = async (transaction: string, extraData: Components.Schemas.Operation[]): Promise<string> =>
(await cbor.encodeAsync([transaction, extraData])).toString('hex');

const decodeExtraData = async (encoded: string): Promise<[string, Components.Schemas.Operation[]]> => {
const [decoded] = await cbor.decodeAll(encoded);
return decoded;
};

const configure = (
cardanoService: CardanoService,
blockService: BlockService,
Expand Down Expand Up @@ -91,7 +112,7 @@ const configure = (
request.network_identifier,
request,
async () => {
const signedTransaction = request.signed_transaction;
const [signedTransaction] = await decodeExtraData(request.signed_transaction);
logger.info('[constructionHash] About to get hash of signed transaction');
const transactionHash = cardanoService.getHashOfSignedTransaction(signedTransaction);
logger.info('[constructionHash] About to return hash of signed transaction');
Expand Down Expand Up @@ -134,8 +155,19 @@ const configure = (
logger.info(operations, '[constuctionPayloads] Operations about to be processed');
const unsignedTransaction = cardanoService.createUnsignedTransaction(operations, ttl);
const payloads = constructPayloadsForTransactionBody(unsignedTransaction.hash, unsignedTransaction.addresses);
// FIXME: we have this as a constant in `block-service`. We should move to a conversion module.
// eslint-disable-next-line camelcase
return { unsigned_transaction: unsignedTransaction.bytes, payloads };
const extraData: Components.Schemas.Operation[] = operations
// eslint-disable-next-line camelcase
.filter(operation => operation.coin_change?.coin_action === 'coin_spent');
// .map(operation => ({ account: operation.account, amount: operation.amount }));
logger.info({ unsignedTransaction, extraData }, '[createUnsignedTransaction] About to return');

return {
// eslint-disable-next-line camelcase
unsigned_transaction: await encodeExtraData(unsignedTransaction.bytes, extraData),
payloads
};
},
logger,
networkId
Expand All @@ -146,16 +178,17 @@ const configure = (
request,
async () => {
logger.info('[constructionCombine] Request received to sign a transaction');
const [transaction, extraData] = await decodeExtraData(request.unsigned_transaction);
const signedTransaction = cardanoService.buildTransaction(
request.unsigned_transaction,
transaction,
request.signatures.map(signature => ({
signature: signature.hex_bytes,
publicKey: signature.public_key.hex_bytes
}))
);
logger.info({ signedTransaction }, '[constructionCombine] About to return signed transaction');
// eslint-disable-next-line camelcase
return { signed_transaction: signedTransaction };
return { signed_transaction: await encodeExtraData(signedTransaction, extraData) };
},
logger,
networkId
Expand All @@ -167,17 +200,20 @@ const configure = (
async () => {
const signed = request.signed;
const networkIdentifier = getNetworkIdentifierByRequestParameters(request.network_identifier);
logger.info(request.transaction, '[constructionParse] Processing');
const [transaction, extraData] = await decodeExtraData(request.transaction);
logger.info({ transaction, extraData }, '[constructionParse] Decoded');
if (signed) {
return {
// eslint-disable-next-line camelcase
network_identifier: request.network_identifier,
...cardanoService.parseSignedTransaction(networkIdentifier, request.transaction)
...cardanoService.parseSignedTransaction(networkIdentifier, transaction, extraData)
};
}
return {
// eslint-disable-next-line camelcase
network_identifier: request.network_identifier,
...cardanoService.parseUnsignedTransaction(networkIdentifier, request.transaction)
...cardanoService.parseUnsignedTransaction(networkIdentifier, transaction, extraData)
};
},
logger,
Expand All @@ -189,7 +225,7 @@ const configure = (
request,
async () => {
try {
const signedTransaction = request.signed_transaction;
const [signedTransaction] = await decodeExtraData(request.signed_transaction);
logger.info(`[constructionSubmit] About to submit ${signedTransaction}`);
await cardanoCli.submitTransaction(signedTransaction, request.network_identifier.network === 'mainnet');
logger.info('[constructionHash] About to get hash of signed transaction');
Expand Down
72 changes: 72 additions & 0 deletions src/server/services/utils/data-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable camelcase */
import cbor from 'cbor';

import { UnsignedTransaction } from '../cardano-services';
import { SIGNATURE_TYPE } from '../../utils/constants';

export const COIN_SPENT_ACTION = 'coin_spent';
export const COIN_CREATED_ACTION = 'coin_created';

interface TransactionExtraData {
account: Components.Schemas.AccountIdentifier | undefined;
amount: Components.Schemas.Amount | undefined;
}

/**
* It maps the transaction body and the addresses to the Rosetta's SigningPayload
* @param transactionBodyHash
* @param addresses
*/
const constructPayloadsForTransactionBody = (
transactionBodyHash: string,
addresses: string[]
): Components.Schemas.SigningPayload[] =>
addresses.map(address => ({ address, hex_bytes: transactionBodyHash, signature_type: SIGNATURE_TYPE }));

/**
* Encodes a standard Cardano unsigned transction alongisde with rosetta-required extra data.
* CBOR is used as it's the Cardano default encoding
*
* @param unsignedTransaction
* @param extraData
* @returns hex encoded unsigned transaction
*/
const encodeUnsignedTransaction = async (
unsignedTransaction: UnsignedTransaction,
extraData: TransactionExtraData[]
): Promise<string> => {
const encoded = await cbor.encodeAsync([unsignedTransaction.bytes, extraData]);
return encoded.toString('hex');
};

/**
* Maps an unsigned transaction to transaction payloads.
*
* As Cardano is a UTXO based blockchain, some information is being lost
* when transaction is encoded. More precisely, input's account and amount
* are not encoded (as it only requires a txid and the output number to be spent).
*
* It might not be a problem although `rosetta-cli` requires this information to
* be present when invoking `/construction/parse` so it needs to be added to our
* responses.
*
* See https://community.rosetta-api.org/t/implementing-the-construction-api-for-utxo-model-coins/100/3
*
* @param unsignedTransaction
* @param operations to be encoded alongside with the transaction
*/
export const mapToPayloads = async (
unsignedTransaction: UnsignedTransaction,
operations: Components.Schemas.Operation[]
): Promise<Components.Schemas.ConstructionPayloadsResponse> => {
const payloads = constructPayloadsForTransactionBody(unsignedTransaction.hash, unsignedTransaction.addresses);
// extra data to be encoded
const extraData: TransactionExtraData[] = operations
.filter(operation => operation.coin_change?.coin_action === COIN_SPENT_ACTION)
.map(operation => ({ account: operation.account, amount: operation.amount }));

return {
unsigned_transaction: await encodeUnsignedTransaction(unsignedTransaction, extraData),
payloads
};
};
Loading

0 comments on commit dd3dbb0

Please sign in to comment.