diff --git a/src/errors/validationError.ts b/src/errors/validationError.ts index 53427d8..a9edbed 100644 --- a/src/errors/validationError.ts +++ b/src/errors/validationError.ts @@ -35,6 +35,7 @@ export enum ValidationErrorReason { REFERENCE_SCRIPT_MUST_NOT_BE_EMPTY_IF_DEFINED = 'Reference script must not be empty if defined', COLLATERAL_RETURN_MUST_NOT_CONTAIN_DATUM = 'Collateral return must not contain datum', COLLATERAL_RETURN_MUST_NOT_CONTAIN_REFERENCE_SCRIPT = 'Collateral return must not contain reference script', + TX_INCONSISTENT_SET_TAGS = 'Inconsistent set tags 258 across the transaction body', } const FIXABLE = true @@ -97,6 +98,7 @@ const validationErrorFixability: Record = { [ValidationErrorReason.COLLATERAL_RETURN_MUST_NOT_CONTAIN_DATUM]: FIXABLE, [ValidationErrorReason.COLLATERAL_RETURN_MUST_NOT_CONTAIN_REFERENCE_SCRIPT]: FIXABLE, + [ValidationErrorReason.TX_INCONSISTENT_SET_TAGS]: FIXABLE, } export type ValidationError = { diff --git a/src/txTransformers.ts b/src/txTransformers.ts index e6d010b..ae8cc2b 100644 --- a/src/txTransformers.ts +++ b/src/txTransformers.ts @@ -5,6 +5,7 @@ import type { FixLenBuffer, Int, Multiasset, + PoolRegistrationCertificate, ReferenceScript, Transaction, TransactionBody, @@ -12,7 +13,7 @@ import type { Uint, Unparsed, } from './types' -import {AmountType, DatumType, TxOutputFormat} from './types' +import {AmountType, CertificateType, DatumType, TxOutputFormat} from './types' import {blake2b256, encodeToCbor, unreachable} from './utils' const transformOptionalList = (optionalList?: T[]): T[] | undefined => @@ -120,22 +121,54 @@ const transformAuxiliaryDataHash = ( ? auxiliaryDataHash : blake2b256(encodeToCbor(auxiliaryData)) +// Add 258 tags everywhere if at least one is present. +// In the future, when the tags are mandatory, +// we should add them everywhere even if not present. +export const makeSetTagsConsistent = ( + txBody: TransactionBody, +): TransactionBody => { + const poolRegistrationCertificate = txBody.certificates?.items.find( + ({type}) => type === CertificateType.POOL_REGISTRATION, + ) as PoolRegistrationCertificate + const allSets = [ + txBody.inputs, + txBody.certificates, + txBody.collateralInputs, + txBody.requiredSigners, + txBody.referenceInputs, + txBody.proposalProcedures, + poolRegistrationCertificate?.poolParams.poolOwners, + ] + const tagIsPresent = allSets.some((s) => s !== undefined && s.hasTag) + if (tagIsPresent) { + allSets.map((s) => { + if (s !== undefined) { + s.hasTag = true + } + }) + } + return txBody +} + export const transformTxBody = ( txBody: TransactionBody, auxiliaryData: Unparsed, -): TransactionBody => ({ - ...txBody, - outputs: txBody.outputs.map(transformTxOutput), - withdrawals: transformOptionalList(txBody.withdrawals), - auxiliaryDataHash: transformAuxiliaryDataHash( - txBody.auxiliaryDataHash, - auxiliaryData, - ), - collateralReturnOutput: - txBody.collateralReturnOutput && - transformTxOutput(txBody.collateralReturnOutput), - votingProcedures: transformOptionalList(txBody.votingProcedures), -}) +): TransactionBody => { + const transformedBody = { + ...txBody, + outputs: txBody.outputs.map(transformTxOutput), + withdrawals: transformOptionalList(txBody.withdrawals), + auxiliaryDataHash: transformAuxiliaryDataHash( + txBody.auxiliaryDataHash, + auxiliaryData, + ), + collateralReturnOutput: + txBody.collateralReturnOutput && + transformTxOutput(txBody.collateralReturnOutput), + votingProcedures: transformOptionalList(txBody.votingProcedures), + } + return makeSetTagsConsistent(transformedBody) +} export const transformTx = (tx: Transaction): Transaction => ({ ...tx, diff --git a/src/txValidators.ts b/src/txValidators.ts index f6cf8eb..12abd6b 100644 --- a/src/txValidators.ts +++ b/src/txValidators.ts @@ -16,6 +16,7 @@ import type { CddlNonEmptySet, CddlNonEmptyOrderedSet, VoterVotes, + PoolRegistrationCertificate, } from './types' import {AmountType, CertificateType, DatumType, TxOutputFormat} from './types' import {bind, unreachable} from './utils' @@ -630,6 +631,27 @@ function* validateTxBody(txBody: TransactionBody): ValidatorReturnType { // extra checks for transactions containing stake pool registration certificates yield* validatePoolRegistrationTransaction(txBody) + + // check for consistency of set tags + const poolRegistrationCertificate = txBody.certificates?.items.find( + ({type}) => type === CertificateType.POOL_REGISTRATION, + ) as PoolRegistrationCertificate + const allSets = [ + txBody.inputs, + txBody.certificates, + txBody.collateralInputs, + txBody.requiredSigners, + txBody.referenceInputs, + txBody.proposalProcedures, + poolRegistrationCertificate?.poolParams.poolOwners, + ] + const tagIsPresent = allSets.some((s) => s !== undefined && s.hasTag) + const tagIsAbsent = allSets.some((s) => s !== undefined && !s.hasTag) + const tagsAreInconsistent = tagIsPresent && tagIsAbsent + yield* validate( + !tagsAreInconsistent, + err(ValidationErrorReason.TX_INCONSISTENT_SET_TAGS, 'transaction_body'), + ) } /** diff --git a/test/integration/__fixtures__/transactions.ts b/test/integration/__fixtures__/transactions.ts index 5e41c4c..a75f374 100644 --- a/test/integration/__fixtures__/transactions.ts +++ b/test/integration/__fixtures__/transactions.ts @@ -1,3 +1,4 @@ +import {ValidationError, ValidationErrorReason} from '../../../src/errors' import { MaxLenBuffer, Port, @@ -587,119 +588,6 @@ export const ValidTransactionBodyTestCases: ValidTransactionBodyTestCase[] = [ donation: undefined, }, }, - { - testName: 'Tx body with tag 258 in collateral inputs and required signers', - cbor: 'a800818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018283583d105e2f080eb93bad86d401545e0ce5f2221096d6477e11e6643922fa8d2ed495234dc0d667c1316ff84e572310e265edb31330448b36b7179e28dd419e1a006ca7935820ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce18883583930167f6dbf610ae030f043adb1f3af78754ed9595ad4ac1f7ed9ff6466760fb6955d1217b1f1f208df6d45ab23c9e17b0c984a2d3a22bbbfb8821a0001e91fa1581cd7a7c6999786354b6dbee181a2f562a628a75fce126f4da40ce5d9b2a146546f6b656e3101582000ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce102182a030a0b5820ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce1880dd90102818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7000ed9010282581cfea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a581ceea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a0f01', - txBody: { - inputs: { - items: [ - { - transactionId: toFixLenBuffer( - '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7', - 32, - ), - index: toUint(0), - }, - ], - hasTag: false, - } as CddlSet, - outputs: [ - { - format: TxOutputFormat.ARRAY_LEGACY, - address: fromBech32( - 'addr_test1zp0z7zqwhya6mpk5q929ur897g3pp9kkgalpreny8y304rfw6j2jxnwq6enuzvt0lp89wgcsufj7mvcnxpzgkd4hz70z3h2pnc8lhq8r', - ), - amount: { - type: AmountType.WITHOUT_MULTIASSET, - coin: toUint(7120787), - }, - datumHash: { - type: DatumType.HASH, - hash: toFixLenBuffer( - 'ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce188', - 32, - ), - }, - }, - { - format: TxOutputFormat.ARRAY_LEGACY, - address: fromBech32( - 'addr_test1xqt87mdlvy9wqv8sgwkmrua00p65ak2ett22c8m7m8lkgenkp7mf2hgjz7clrusgmak5t2ere8shkrycfgkn5g4mh7uqvcq039', - ), - amount: { - type: AmountType.WITH_MULTIASSET, - coin: toUint(125215), - multiasset: [ - { - policyId: toFixLenBuffer( - 'd7a7c6999786354b6dbee181a2f562a628a75fce126f4da40ce5d9b2', - 28, - ), - tokens: [ - { - assetName: Buffer.from('Token1') as MaxLenBuffer<32>, - amount: toUint(1), - }, - ], - }, - ], - }, - datumHash: { - type: DatumType.HASH, - hash: toFixLenBuffer( - '00ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce1', - 32, - ), - }, - }, - ], - fee: toUint(42), - ttl: toUint(10), - certificates: undefined, - withdrawals: undefined, - update: undefined, - auxiliaryDataHash: undefined, - validityIntervalStart: undefined, - mint: undefined, - scriptDataHash: toFixLenBuffer( - 'ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce188', - 32, - ), - collateralInputs: { - items: [ - { - transactionId: toFixLenBuffer( - '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7', - 32, - ), - index: toUint(0), - }, - ], - hasTag: true, - } as CddlNonEmptySet, - requiredSigners: { - items: [ - toFixLenBuffer( - 'fea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a', - 28, - ), - toFixLenBuffer( - 'eea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a', - 28, - ), - ], - hasTag: true, - } as CddlNonEmptySet, - networkId: toUint(1), - collateralReturnOutput: undefined, - totalCollateral: undefined, - referenceInputs: undefined, - votingProcedures: undefined, - proposalProcedures: undefined, - treasury: undefined, - donation: undefined, - }, - }, { testName: 'Tx body with inline datum, reference script, collateral return, total collateral and reference input', @@ -1170,6 +1058,7 @@ export const ValidTransactionBodyTestCases: ValidTransactionBodyTestCase[] = [ type TransformTransactionBodyTestCase = { testName: string cbor: string + validationErrors: ValidationError[] auxiliaryData?: unknown txBody: TransactionBody } @@ -1179,6 +1068,7 @@ export const TransformTransactionTestCases: TransformTransactionBodyTestCase[] = { testName: 'Simple tx body with canonical auxiliary data', cbor: 'a50081825820bc8bf52ea894fb8e442fe3eea628be87d0c9a37baef185b70eb00a5c8a849d3b000181825839000743d16cfe3c4fcc0c11c2403bbc10dbc7ecdd4477e053481a368e7a06e2ae44dff6770dc0f4ada3cf4cf2605008e27aecdb332ad349fda71a0023583c021a00029b75031a01a3bd8f075820fb7099a47afd6efb4f9cccf9d0f8745331a19eb8b3f50548ffadae9de8551743', + validationErrors: [], auxiliaryData: CanonicalAuxiliaryData.data, txBody: { inputs: { @@ -1230,6 +1120,7 @@ export const TransformTransactionTestCases: TransformTransactionBodyTestCase[] = { testName: 'Simple tx body with non canonical auxiliary data', cbor: 'a50081825820bc8bf52ea894fb8e442fe3eea628be87d0c9a37baef185b70eb00a5c8a849d3b000181825839000743d16cfe3c4fcc0c11c2403bbc10dbc7ecdd4477e053481a368e7a06e2ae44dff6770dc0f4ada3cf4cf2605008e27aecdb332ad349fda71a0023583c021a00029b75031a01a3bd8f075820fb7099a47afd6efb4f9cccf9d0f8745331a19eb8b3f50548ffadae9de8551743', + validationErrors: [], auxiliaryData: NonCanonicalAuxiliaryData.data, txBody: { inputs: { @@ -1281,6 +1172,7 @@ export const TransformTransactionTestCases: TransformTransactionBodyTestCase[] = { testName: 'Simple tx body with auxiliary data hash but no auxiliary data', cbor: 'a50081825820bc8bf52ea894fb8e442fe3eea628be87d0c9a37baef185b70eb00a5c8a849d3b000181825839000743d16cfe3c4fcc0c11c2403bbc10dbc7ecdd4477e053481a368e7a06e2ae44dff6770dc0f4ada3cf4cf2605008e27aecdb332ad349fda71a0023583c021a00029b75031a01a3bd8f075820fb7099a47afd6efb4f9cccf9d0f8745331a19eb8b3f50548ffadae9de8551743', + validationErrors: [], auxiliaryData: null, txBody: { inputs: { @@ -1332,6 +1224,127 @@ export const TransformTransactionTestCases: TransformTransactionBodyTestCase[] = donation: undefined, }, }, + { + testName: + 'Tx body with tag 258 in collateral inputs and required signers, but not inputs', + cbor: 'a800818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018283583d105e2f080eb93bad86d401545e0ce5f2221096d6477e11e6643922fa8d2ed495234dc0d667c1316ff84e572310e265edb31330448b36b7179e28dd419e1a006ca7935820ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce18883583930167f6dbf610ae030f043adb1f3af78754ed9595ad4ac1f7ed9ff6466760fb6955d1217b1f1f208df6d45ab23c9e17b0c984a2d3a22bbbfb8821a0001e91fa1581cd7a7c6999786354b6dbee181a2f562a628a75fce126f4da40ce5d9b2a146546f6b656e3101582000ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce102182a030a0b5820ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce1880dd90102818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7000ed9010282581cfea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a581ceea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a0f01', + validationErrors: [ + { + fixable: true, + reason: ValidationErrorReason.TX_INCONSISTENT_SET_TAGS, + position: 'transaction_body', + }, + ], + txBody: { + inputs: { + items: [ + { + transactionId: toFixLenBuffer( + '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7', + 32, + ), + index: toUint(0), + }, + ], + hasTag: true, + } as CddlSet, + outputs: [ + { + format: TxOutputFormat.ARRAY_LEGACY, + address: fromBech32( + 'addr_test1zp0z7zqwhya6mpk5q929ur897g3pp9kkgalpreny8y304rfw6j2jxnwq6enuzvt0lp89wgcsufj7mvcnxpzgkd4hz70z3h2pnc8lhq8r', + ), + amount: { + type: AmountType.WITHOUT_MULTIASSET, + coin: toUint(7120787), + }, + datumHash: { + type: DatumType.HASH, + hash: toFixLenBuffer( + 'ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce188', + 32, + ), + }, + }, + { + format: TxOutputFormat.ARRAY_LEGACY, + address: fromBech32( + 'addr_test1xqt87mdlvy9wqv8sgwkmrua00p65ak2ett22c8m7m8lkgenkp7mf2hgjz7clrusgmak5t2ere8shkrycfgkn5g4mh7uqvcq039', + ), + amount: { + type: AmountType.WITH_MULTIASSET, + coin: toUint(125215), + multiasset: [ + { + policyId: toFixLenBuffer( + 'd7a7c6999786354b6dbee181a2f562a628a75fce126f4da40ce5d9b2', + 28, + ), + tokens: [ + { + assetName: Buffer.from('Token1') as MaxLenBuffer<32>, + amount: toUint(1), + }, + ], + }, + ], + }, + datumHash: { + type: DatumType.HASH, + hash: toFixLenBuffer( + '00ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce1', + 32, + ), + }, + }, + ], + fee: toUint(42), + ttl: toUint(10), + certificates: undefined, + withdrawals: undefined, + update: undefined, + auxiliaryDataHash: undefined, + validityIntervalStart: undefined, + mint: undefined, + scriptDataHash: toFixLenBuffer( + 'ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce188', + 32, + ), + collateralInputs: { + items: [ + { + transactionId: toFixLenBuffer( + '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7', + 32, + ), + index: toUint(0), + }, + ], + hasTag: true, + } as CddlNonEmptySet, + requiredSigners: { + items: [ + toFixLenBuffer( + 'fea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a', + 28, + ), + toFixLenBuffer( + 'eea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a', + 28, + ), + ], + hasTag: true, + } as CddlNonEmptySet, + networkId: toUint(1), + collateralReturnOutput: undefined, + totalCollateral: undefined, + referenceInputs: undefined, + votingProcedures: undefined, + proposalProcedures: undefined, + treasury: undefined, + donation: undefined, + }, + }, ] type ValidTransactionTestCase = { @@ -1689,4 +1702,123 @@ export const ValidTransactionTestCases: ValidTransactionTestCase[] = [ auxiliaryData: null, }, }, + { + testName: + 'Tx with tag 258 in collateral inputs and required signers, but not inputs', + cbor: '84a800818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018283583d105e2f080eb93bad86d401545e0ce5f2221096d6477e11e6643922fa8d2ed495234dc0d667c1316ff84e572310e265edb31330448b36b7179e28dd419e1a006ca7935820ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce18883583930167f6dbf610ae030f043adb1f3af78754ed9595ad4ac1f7ed9ff6466760fb6955d1217b1f1f208df6d45ab23c9e17b0c984a2d3a22bbbfb8821a0001e91fa1581cd7a7c6999786354b6dbee181a2f562a628a75fce126f4da40ce5d9b2a146546f6b656e3101582000ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce102182a030a0b5820ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce1880dd90102818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7000ed9010282581cfea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a581ceea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a0f01a0f5f6', + tx: { + body: { + inputs: { + items: [ + { + transactionId: toFixLenBuffer( + '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7', + 32, + ), + index: toUint(0), + }, + ], + hasTag: false, + } as CddlSet, + outputs: [ + { + format: TxOutputFormat.ARRAY_LEGACY, + address: fromBech32( + 'addr_test1zp0z7zqwhya6mpk5q929ur897g3pp9kkgalpreny8y304rfw6j2jxnwq6enuzvt0lp89wgcsufj7mvcnxpzgkd4hz70z3h2pnc8lhq8r', + ), + amount: { + type: AmountType.WITHOUT_MULTIASSET, + coin: toUint(7120787), + }, + datumHash: { + type: DatumType.HASH, + hash: toFixLenBuffer( + 'ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce188', + 32, + ), + }, + }, + { + format: TxOutputFormat.ARRAY_LEGACY, + address: fromBech32( + 'addr_test1xqt87mdlvy9wqv8sgwkmrua00p65ak2ett22c8m7m8lkgenkp7mf2hgjz7clrusgmak5t2ere8shkrycfgkn5g4mh7uqvcq039', + ), + amount: { + type: AmountType.WITH_MULTIASSET, + coin: toUint(125215), + multiasset: [ + { + policyId: toFixLenBuffer( + 'd7a7c6999786354b6dbee181a2f562a628a75fce126f4da40ce5d9b2', + 28, + ), + tokens: [ + { + assetName: Buffer.from('Token1') as MaxLenBuffer<32>, + amount: toUint(1), + }, + ], + }, + ], + }, + datumHash: { + type: DatumType.HASH, + hash: toFixLenBuffer( + '00ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce1', + 32, + ), + }, + }, + ], + fee: toUint(42), + ttl: toUint(10), + certificates: undefined, + withdrawals: undefined, + update: undefined, + auxiliaryDataHash: undefined, + validityIntervalStart: undefined, + mint: undefined, + scriptDataHash: toFixLenBuffer( + 'ffd4d009f554ba4fd8ed1f1d703244819861a9d34fd4753bcf3ff32f043ce188', + 32, + ), + collateralInputs: { + items: [ + { + transactionId: toFixLenBuffer( + '3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7', + 32, + ), + index: toUint(0), + }, + ], + hasTag: true, + } as CddlNonEmptySet, + requiredSigners: { + items: [ + toFixLenBuffer( + 'fea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a', + 28, + ), + toFixLenBuffer( + 'eea6646c67fb467f8a5425e9c752e1e262b0420ba4b638f39514049a', + 28, + ), + ], + hasTag: true, + } as CddlNonEmptySet, + networkId: toUint(1), + collateralReturnOutput: undefined, + totalCollateral: undefined, + referenceInputs: undefined, + votingProcedures: undefined, + proposalProcedures: undefined, + treasury: undefined, + donation: undefined, + }, + witnessSet: {}, + scriptValidity: true, + auxiliaryData: null, + }, + }, ] diff --git a/test/integration/transform.test.ts b/test/integration/transform.test.ts index 66e24b5..5437761 100644 --- a/test/integration/transform.test.ts +++ b/test/integration/transform.test.ts @@ -1,6 +1,6 @@ import {expect} from 'chai' -import {decodeTxBody, transformTxBody} from '../../src/index' +import {decodeTxBody, transformTxBody, validateTxBody} from '../../src/index' import {TransformTransactionTestCases} from './__fixtures__/transactions' describe('Transform', () => { @@ -19,3 +19,16 @@ describe('Transform', () => { }) } }) + +describe('Validate', () => { + for (const { + testName, + cbor, + validationErrors, + } of TransformTransactionTestCases) { + it(testName, () => { + const errors = validateTxBody(Buffer.from(cbor, 'hex')) + expect(errors).to.deep.equal(validationErrors) + }) + } +})