From 255d0d5c6ff733813ce7829a2a53ea40eab63722 Mon Sep 17 00:00:00 2001 From: Riley-Kilgore Date: Wed, 12 Feb 2025 10:46:09 -0800 Subject: [PATCH] fix: Added nativescript checks for ownSignatureKeyPaths --- .../key-management/src/InMemoryKeyAgent.ts | 4 +- packages/key-management/src/types.ts | 1 + .../src/util/ownSignatureKeyPaths.ts | 81 ++++++++++++++++++- .../src/util/stubSignTransaction.ts | 4 +- .../test/InMemoryKeyAgent.test.ts | 3 +- .../test/util/ownSignaturePaths.test.ts | 74 ++++++++++++++--- .../test/util/stubSignTransaction.test.ts | 3 +- 7 files changed, 153 insertions(+), 17 deletions(-) diff --git a/packages/key-management/src/InMemoryKeyAgent.ts b/packages/key-management/src/InMemoryKeyAgent.ts index 8a359c549d8..d7efb18f805 100644 --- a/packages/key-management/src/InMemoryKeyAgent.ts +++ b/packages/key-management/src/InMemoryKeyAgent.ts @@ -134,7 +134,7 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent { async signTransaction( txBody: Serialization.TransactionBody, - { txInKeyPathMap, knownAddresses }: SignTransactionContext, + { txInKeyPathMap, knownAddresses, scripts }: SignTransactionContext, { additionalKeyPaths = [] }: SignTransactionOptions = {} ): Promise { // Possible optimization is casting strings to OpaqueString types directly and skipping validation @@ -143,7 +143,7 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent { const dRepKeyHash = ( await Crypto.Ed25519PublicKey.fromHex(await this.derivePublicKey(DREP_KEY_DERIVATION_PATH)).hash() ).hex(); - const derivationPaths = ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap, dRepKeyHash); + const derivationPaths = ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap, dRepKeyHash, scripts); const keyPaths = uniqBy([...derivationPaths, ...additionalKeyPaths], ({ role, index }) => `${role}.${index}`); // TODO: // if (keyPaths.length === 0) { diff --git a/packages/key-management/src/types.ts b/packages/key-management/src/types.ts index 28a057f5ae1..720df46c314 100644 --- a/packages/key-management/src/types.ts +++ b/packages/key-management/src/types.ts @@ -175,6 +175,7 @@ export interface SignTransactionContext { handleResolutions?: HandleResolution[]; dRepKeyHashHex?: Crypto.Ed25519KeyHashHex; sender?: MessageSender; + scripts?: Cardano.Script[]; } export type SignDataContext = Cip8SignDataContext & { sender?: MessageSender }; diff --git a/packages/key-management/src/util/ownSignatureKeyPaths.ts b/packages/key-management/src/util/ownSignatureKeyPaths.ts index 9c5054f4888..18c9c1806eb 100644 --- a/packages/key-management/src/util/ownSignatureKeyPaths.ts +++ b/packages/key-management/src/util/ownSignatureKeyPaths.ts @@ -294,6 +294,81 @@ const getRequiredSignersKeyPaths = ( return paths; }; +const checkStakeCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519KeyHashHex): SignatureCheck => + address.stakeKeyDerivationPath && + Cardano.RewardAccount.toHash(address.rewardAccount) === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(keyHash) + ? { derivationPaths: [address.stakeKeyDerivationPath], requiresForeignSignatures: false } + : { derivationPaths: [], requiresForeignSignatures: true }; + +const checkPaymentCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519KeyHashHex): SignatureCheck => { + const paymentCredential = Cardano.Address.fromBech32(address.address)?.asBase()?.getPaymentCredential(); + return paymentCredential?.type === Cardano.CredentialType.KeyHash && + paymentCredential.hash === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(keyHash) + ? { + derivationPaths: [{ index: address.index, role: Number(address.type) }], + requiresForeignSignatures: false + } + : { derivationPaths: [], requiresForeignSignatures: true }; +}; + +const combineSignatureChecks = (a: SignatureCheck, b: SignatureCheck): SignatureCheck => ({ + derivationPaths: [...a.derivationPaths, ...b.derivationPaths], + requiresForeignSignatures: a.requiresForeignSignatures || b.requiresForeignSignatures +}); + +const processSignatureScript = ( + script: Cardano.RequireSignatureScript, + groupedAddresses: GroupedAddress[] +): SignatureCheck => { + let signatureCheck: SignatureCheck = { derivationPaths: [], requiresForeignSignatures: false }; + + for (const address of groupedAddresses) { + if (address.stakeKeyDerivationPath) { + signatureCheck = checkStakeCredential(address, script.keyHash); + } + signatureCheck = combineSignatureChecks(signatureCheck, checkPaymentCredential(address, script.keyHash)); + } + + return signatureCheck; +}; + +const getNativeScriptKeyPaths = ( + groupedAddresses: GroupedAddress[], + nativeScripts?: Cardano.Script[] +): SignatureCheck => { + const signatureCheck: SignatureCheck = { derivationPaths: [], requiresForeignSignatures: false }; + if (!nativeScripts?.length) return signatureCheck; + + const processScript = (script: Cardano.Script): SignatureCheck => { + if (!Cardano.isNativeScript(script)) { + return { derivationPaths: [], requiresForeignSignatures: false }; + } + + switch (script.kind) { + case Cardano.NativeScriptKind.RequireSignature: { + return processSignatureScript(script as Cardano.RequireSignatureScript, groupedAddresses); + } + case Cardano.NativeScriptKind.RequireAllOf: + case Cardano.NativeScriptKind.RequireAnyOf: + case Cardano.NativeScriptKind.RequireNOf: { + const scriptWithScripts = script as Cardano.RequireAllOfScript | Cardano.RequireAnyOfScript; + return scriptWithScripts.scripts.reduce( + (acc, subScript) => combineSignatureChecks(acc, processScript(subScript)), + { derivationPaths: [], requiresForeignSignatures: false } + ); + } + case Cardano.NativeScriptKind.RequireTimeBefore: + case Cardano.NativeScriptKind.RequireTimeAfter: + return { derivationPaths: [], requiresForeignSignatures: false }; + } + }; + + return nativeScripts.reduce( + (acc, script) => combineSignatureChecks(acc, processScript(script)), + signatureCheck + ); +}; + /** Check if there are certificates that require DRep credentials and if we own them */ export const getDRepCredentialKeyPaths = ({ dRepKeyHash, @@ -357,7 +432,8 @@ export const ownSignatureKeyPaths = ( txBody: Cardano.TxBody, knownAddresses: GroupedAddress[], txInKeyPathMap: TxInKeyPathMap, - dRepKeyHash?: Crypto.Ed25519KeyHashHex + dRepKeyHash?: Crypto.Ed25519KeyHashHex, + scripts?: Cardano.Script[] ): AccountKeyDerivationPath[] => { // TODO: add `proposal_procedure` witnesses. @@ -368,7 +444,8 @@ export const ownSignatureKeyPaths = ( ...getStakeCredentialKeyPaths(knownAddresses, txBody).derivationPaths, ...getDRepCredentialKeyPaths({ dRepKeyHash, txBody }).derivationPaths, ...getRequiredSignersKeyPaths(knownAddresses, txBody.requiredExtraSignatures), - ...getVotingProcedureKeyPaths({ dRepKeyHash, groupedAddresses: knownAddresses, txBody }).derivationPaths + ...getVotingProcedureKeyPaths({ dRepKeyHash, groupedAddresses: knownAddresses, txBody }).derivationPaths, + ...getNativeScriptKeyPaths(knownAddresses, scripts).derivationPaths ], isEqual ); diff --git a/packages/key-management/src/util/stubSignTransaction.ts b/packages/key-management/src/util/stubSignTransaction.ts index 03d6db3312e..f3788018454 100644 --- a/packages/key-management/src/util/stubSignTransaction.ts +++ b/packages/key-management/src/util/stubSignTransaction.ts @@ -17,7 +17,7 @@ export interface StubSignTransactionProps { export const stubSignTransaction = async ({ txBody, - context: { knownAddresses, txInKeyPathMap, dRepKeyHashHex: dRepKeyHash }, + context: { knownAddresses, txInKeyPathMap, dRepKeyHashHex: dRepKeyHash, scripts }, signTransactionOptions: { extraSigners, additionalKeyPaths = [] } = {} }: StubSignTransactionProps): Promise => { const mockSignature = Crypto.Ed25519SignatureHex( @@ -25,7 +25,7 @@ export const stubSignTransaction = async ({ 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' ); const signatureKeyPaths = uniqWith( - [...ownSignatureKeyPaths(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash), ...additionalKeyPaths], + [...ownSignatureKeyPaths(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash, scripts), ...additionalKeyPaths], deepEquals ); diff --git a/packages/key-management/test/InMemoryKeyAgent.test.ts b/packages/key-management/test/InMemoryKeyAgent.test.ts index 17ddb2da7ce..78c2d8e0276 100644 --- a/packages/key-management/test/InMemoryKeyAgent.test.ts +++ b/packages/key-management/test/InMemoryKeyAgent.test.ts @@ -111,7 +111,8 @@ describe('InMemoryKeyAgent', () => { knownAddresses, txInKeyPathMap }); - expect(ownSignatureKeyPaths).toBeCalledWith(body.toCore(), knownAddresses, txInKeyPathMap, expect.anything()); + const expectedArgs = [body.toCore(), knownAddresses, txInKeyPathMap, expect.anything(), undefined] as const; + expect(ownSignatureKeyPaths).toBeCalledWith(...expectedArgs); expect(witnessSet.size).toBe(2); expect(typeof [...witnessSet.values()][0]).toBe('string'); }); diff --git a/packages/key-management/test/util/ownSignaturePaths.test.ts b/packages/key-management/test/util/ownSignaturePaths.test.ts index b09597dc6d7..41d3ad04fc7 100644 --- a/packages/key-management/test/util/ownSignaturePaths.test.ts +++ b/packages/key-management/test/util/ownSignaturePaths.test.ts @@ -16,25 +16,33 @@ const toStakeCredential = (stakeKeyHash: Crypto.Hash28ByteBase16): Cardano.Crede type: Cardano.CredentialType.KeyHash }); +const createBaseGroupedAddress = ( + address: Cardano.PaymentAddress, + rewardAccount: Cardano.RewardAccount, + type: AddressType, + index: number +) => ({ + address, + index, + rewardAccount, + type +}); + const createGroupedAddress = ( address: Cardano.PaymentAddress, rewardAccount: Cardano.RewardAccount, type: AddressType, index: number, stakeKeyDerivationPath: AccountKeyDerivationPath - // eslint-disable-next-line max-params -): GroupedAddress => +) => ({ - address, - index, - rewardAccount, - stakeKeyDerivationPath, - type + ...createBaseGroupedAddress(address, rewardAccount, type, index), + stakeKeyDerivationPath } as GroupedAddress); -describe('KeyManagement.util.ownSignaturePaths', () => { - const ownRewardAccount = Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27'); +const createTestAddresses = () => { const otherRewardAccount = Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); + const ownRewardAccount = Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27'); const address1 = Cardano.PaymentAddress( 'addr_test1qra788mu4sg8kwd93ns9nfdh3k4ufxwg4xhz2r3n064tzfgxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flkns6cy45x' ); @@ -42,6 +50,11 @@ describe('KeyManagement.util.ownSignaturePaths', () => { 'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp' ); + return { address1, address2, otherRewardAccount, ownRewardAccount }; +}; + +describe('KeyManagement.util.ownSignaturePaths', () => { + const { address1, address2, ownRewardAccount, otherRewardAccount } = createTestAddresses(); const ownStakeKeyHash = Cardano.RewardAccount.toHash(ownRewardAccount); const ownStakeCredential = { hash: ownStakeKeyHash, @@ -578,4 +591,47 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ]); }); }); + + describe('Native scripts', () => { + it('includes derivation paths from native scripts when scripts are provided', async () => { + const txBody: Cardano.TxBody = { + fee: BigInt(0), + inputs: [{}, {}, {}] as Cardano.TxIn[], + outputs: [] + }; + + const scripts: Cardano.Script[] = [ + { + __type: Cardano.ScriptType.Native, + keyHash: Ed25519KeyHashHex(ownStakeKeyHash), + kind: Cardano.NativeScriptKind.RequireSignature + } + ]; + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, undefined, scripts)).toEqual([ + { + index: 0, + role: KeyRole.Stake + } + ]); + }); + + it('does not include derivation paths from native scripts with foreign key hashes', async () => { + const txBody: Cardano.TxBody = { + fee: BigInt(0), + inputs: [{}, {}, {}] as Cardano.TxIn[], + outputs: [] + }; + + const scripts: Cardano.Script[] = [ + { + __type: Cardano.ScriptType.Native, + keyHash: Ed25519KeyHashHex(otherStakeKeyHash), + kind: Cardano.NativeScriptKind.RequireSignature + } + ]; + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, undefined, scripts)).toEqual([]); + }); + }); }); diff --git a/packages/key-management/test/util/stubSignTransaction.test.ts b/packages/key-management/test/util/stubSignTransaction.test.ts index 32463417171..462da46cb2b 100644 --- a/packages/key-management/test/util/stubSignTransaction.test.ts +++ b/packages/key-management/test/util/stubSignTransaction.test.ts @@ -34,6 +34,7 @@ describe('KeyManagement.util.stubSignTransaction', () => { }) ).size ).toBe(2); - expect(ownSignatureKeyPaths).toBeCalledWith(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash); + const expectedArgs = [txBody, knownAddresses, txInKeyPathMap, dRepKeyHash, undefined] as const; + expect(ownSignatureKeyPaths).toBeCalledWith(...expectedArgs); }); });