Skip to content

Commit d3f03b2

Browse files
Merge pull request #1599 from Riley-Kilgore/fix/nativescript-signing
NativeScript signing
2 parents c1d3304 + 255d0d5 commit d3f03b2

7 files changed

+153
-17
lines changed

packages/key-management/src/InMemoryKeyAgent.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent {
134134

135135
async signTransaction(
136136
txBody: Serialization.TransactionBody,
137-
{ txInKeyPathMap, knownAddresses }: SignTransactionContext,
137+
{ txInKeyPathMap, knownAddresses, scripts }: SignTransactionContext,
138138
{ additionalKeyPaths = [] }: SignTransactionOptions = {}
139139
): Promise<Cardano.Signatures> {
140140
// Possible optimization is casting strings to OpaqueString types directly and skipping validation
@@ -143,7 +143,7 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent {
143143
const dRepKeyHash = (
144144
await Crypto.Ed25519PublicKey.fromHex(await this.derivePublicKey(DREP_KEY_DERIVATION_PATH)).hash()
145145
).hex();
146-
const derivationPaths = ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap, dRepKeyHash);
146+
const derivationPaths = ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap, dRepKeyHash, scripts);
147147
const keyPaths = uniqBy([...derivationPaths, ...additionalKeyPaths], ({ role, index }) => `${role}.${index}`);
148148
// TODO:
149149
// if (keyPaths.length === 0) {

packages/key-management/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export interface SignTransactionContext {
175175
handleResolutions?: HandleResolution[];
176176
dRepKeyHashHex?: Crypto.Ed25519KeyHashHex;
177177
sender?: MessageSender;
178+
scripts?: Cardano.Script[];
178179
}
179180

180181
export type SignDataContext = Cip8SignDataContext & { sender?: MessageSender };

packages/key-management/src/util/ownSignatureKeyPaths.ts

+79-2
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,81 @@ const getRequiredSignersKeyPaths = (
294294
return paths;
295295
};
296296

297+
const checkStakeCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519KeyHashHex): SignatureCheck =>
298+
address.stakeKeyDerivationPath &&
299+
Cardano.RewardAccount.toHash(address.rewardAccount) === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(keyHash)
300+
? { derivationPaths: [address.stakeKeyDerivationPath], requiresForeignSignatures: false }
301+
: { derivationPaths: [], requiresForeignSignatures: true };
302+
303+
const checkPaymentCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519KeyHashHex): SignatureCheck => {
304+
const paymentCredential = Cardano.Address.fromBech32(address.address)?.asBase()?.getPaymentCredential();
305+
return paymentCredential?.type === Cardano.CredentialType.KeyHash &&
306+
paymentCredential.hash === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(keyHash)
307+
? {
308+
derivationPaths: [{ index: address.index, role: Number(address.type) }],
309+
requiresForeignSignatures: false
310+
}
311+
: { derivationPaths: [], requiresForeignSignatures: true };
312+
};
313+
314+
const combineSignatureChecks = (a: SignatureCheck, b: SignatureCheck): SignatureCheck => ({
315+
derivationPaths: [...a.derivationPaths, ...b.derivationPaths],
316+
requiresForeignSignatures: a.requiresForeignSignatures || b.requiresForeignSignatures
317+
});
318+
319+
const processSignatureScript = (
320+
script: Cardano.RequireSignatureScript,
321+
groupedAddresses: GroupedAddress[]
322+
): SignatureCheck => {
323+
let signatureCheck: SignatureCheck = { derivationPaths: [], requiresForeignSignatures: false };
324+
325+
for (const address of groupedAddresses) {
326+
if (address.stakeKeyDerivationPath) {
327+
signatureCheck = checkStakeCredential(address, script.keyHash);
328+
}
329+
signatureCheck = combineSignatureChecks(signatureCheck, checkPaymentCredential(address, script.keyHash));
330+
}
331+
332+
return signatureCheck;
333+
};
334+
335+
const getNativeScriptKeyPaths = (
336+
groupedAddresses: GroupedAddress[],
337+
nativeScripts?: Cardano.Script[]
338+
): SignatureCheck => {
339+
const signatureCheck: SignatureCheck = { derivationPaths: [], requiresForeignSignatures: false };
340+
if (!nativeScripts?.length) return signatureCheck;
341+
342+
const processScript = (script: Cardano.Script): SignatureCheck => {
343+
if (!Cardano.isNativeScript(script)) {
344+
return { derivationPaths: [], requiresForeignSignatures: false };
345+
}
346+
347+
switch (script.kind) {
348+
case Cardano.NativeScriptKind.RequireSignature: {
349+
return processSignatureScript(script as Cardano.RequireSignatureScript, groupedAddresses);
350+
}
351+
case Cardano.NativeScriptKind.RequireAllOf:
352+
case Cardano.NativeScriptKind.RequireAnyOf:
353+
case Cardano.NativeScriptKind.RequireNOf: {
354+
const scriptWithScripts = script as Cardano.RequireAllOfScript | Cardano.RequireAnyOfScript;
355+
return scriptWithScripts.scripts.reduce<SignatureCheck>(
356+
(acc, subScript) => combineSignatureChecks(acc, processScript(subScript)),
357+
{ derivationPaths: [], requiresForeignSignatures: false }
358+
);
359+
}
360+
case Cardano.NativeScriptKind.RequireTimeBefore:
361+
case Cardano.NativeScriptKind.RequireTimeAfter:
362+
return { derivationPaths: [], requiresForeignSignatures: false };
363+
}
364+
};
365+
366+
return nativeScripts.reduce<SignatureCheck>(
367+
(acc, script) => combineSignatureChecks(acc, processScript(script)),
368+
signatureCheck
369+
);
370+
};
371+
297372
/** Check if there are certificates that require DRep credentials and if we own them */
298373
export const getDRepCredentialKeyPaths = ({
299374
dRepKeyHash,
@@ -357,7 +432,8 @@ export const ownSignatureKeyPaths = (
357432
txBody: Cardano.TxBody,
358433
knownAddresses: GroupedAddress[],
359434
txInKeyPathMap: TxInKeyPathMap,
360-
dRepKeyHash?: Crypto.Ed25519KeyHashHex
435+
dRepKeyHash?: Crypto.Ed25519KeyHashHex,
436+
scripts?: Cardano.Script[]
361437
): AccountKeyDerivationPath[] => {
362438
// TODO: add `proposal_procedure` witnesses.
363439

@@ -368,7 +444,8 @@ export const ownSignatureKeyPaths = (
368444
...getStakeCredentialKeyPaths(knownAddresses, txBody).derivationPaths,
369445
...getDRepCredentialKeyPaths({ dRepKeyHash, txBody }).derivationPaths,
370446
...getRequiredSignersKeyPaths(knownAddresses, txBody.requiredExtraSignatures),
371-
...getVotingProcedureKeyPaths({ dRepKeyHash, groupedAddresses: knownAddresses, txBody }).derivationPaths
447+
...getVotingProcedureKeyPaths({ dRepKeyHash, groupedAddresses: knownAddresses, txBody }).derivationPaths,
448+
...getNativeScriptKeyPaths(knownAddresses, scripts).derivationPaths
372449
],
373450
isEqual
374451
);

packages/key-management/src/util/stubSignTransaction.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ export interface StubSignTransactionProps {
1717

1818
export const stubSignTransaction = async ({
1919
txBody,
20-
context: { knownAddresses, txInKeyPathMap, dRepKeyHashHex: dRepKeyHash },
20+
context: { knownAddresses, txInKeyPathMap, dRepKeyHashHex: dRepKeyHash, scripts },
2121
signTransactionOptions: { extraSigners, additionalKeyPaths = [] } = {}
2222
}: StubSignTransactionProps): Promise<Cardano.Signatures> => {
2323
const mockSignature = Crypto.Ed25519SignatureHex(
2424
// eslint-disable-next-line max-len
2525
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
2626
);
2727
const signatureKeyPaths = uniqWith(
28-
[...ownSignatureKeyPaths(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash), ...additionalKeyPaths],
28+
[...ownSignatureKeyPaths(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash, scripts), ...additionalKeyPaths],
2929
deepEquals
3030
);
3131

packages/key-management/test/InMemoryKeyAgent.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ describe('InMemoryKeyAgent', () => {
111111
knownAddresses,
112112
txInKeyPathMap
113113
});
114-
expect(ownSignatureKeyPaths).toBeCalledWith(body.toCore(), knownAddresses, txInKeyPathMap, expect.anything());
114+
const expectedArgs = [body.toCore(), knownAddresses, txInKeyPathMap, expect.anything(), undefined] as const;
115+
expect(ownSignatureKeyPaths).toBeCalledWith(...expectedArgs);
115116
expect(witnessSet.size).toBe(2);
116117
expect(typeof [...witnessSet.values()][0]).toBe('string');
117118
});

packages/key-management/test/util/ownSignaturePaths.test.ts

+65-9
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,45 @@ const toStakeCredential = (stakeKeyHash: Crypto.Hash28ByteBase16): Cardano.Crede
1616
type: Cardano.CredentialType.KeyHash
1717
});
1818

19+
const createBaseGroupedAddress = (
20+
address: Cardano.PaymentAddress,
21+
rewardAccount: Cardano.RewardAccount,
22+
type: AddressType,
23+
index: number
24+
) => ({
25+
address,
26+
index,
27+
rewardAccount,
28+
type
29+
});
30+
1931
const createGroupedAddress = (
2032
address: Cardano.PaymentAddress,
2133
rewardAccount: Cardano.RewardAccount,
2234
type: AddressType,
2335
index: number,
2436
stakeKeyDerivationPath: AccountKeyDerivationPath
25-
// eslint-disable-next-line max-params
26-
): GroupedAddress =>
37+
) =>
2738
({
28-
address,
29-
index,
30-
rewardAccount,
31-
stakeKeyDerivationPath,
32-
type
39+
...createBaseGroupedAddress(address, rewardAccount, type, index),
40+
stakeKeyDerivationPath
3341
} as GroupedAddress);
3442

35-
describe('KeyManagement.util.ownSignaturePaths', () => {
36-
const ownRewardAccount = Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27');
43+
const createTestAddresses = () => {
3744
const otherRewardAccount = Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj');
45+
const ownRewardAccount = Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27');
3846
const address1 = Cardano.PaymentAddress(
3947
'addr_test1qra788mu4sg8kwd93ns9nfdh3k4ufxwg4xhz2r3n064tzfgxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flkns6cy45x'
4048
);
4149
const address2 = Cardano.PaymentAddress(
4250
'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp'
4351
);
4452

53+
return { address1, address2, otherRewardAccount, ownRewardAccount };
54+
};
55+
56+
describe('KeyManagement.util.ownSignaturePaths', () => {
57+
const { address1, address2, ownRewardAccount, otherRewardAccount } = createTestAddresses();
4558
const ownStakeKeyHash = Cardano.RewardAccount.toHash(ownRewardAccount);
4659
const ownStakeCredential = {
4760
hash: ownStakeKeyHash,
@@ -578,4 +591,47 @@ describe('KeyManagement.util.ownSignaturePaths', () => {
578591
]);
579592
});
580593
});
594+
595+
describe('Native scripts', () => {
596+
it('includes derivation paths from native scripts when scripts are provided', async () => {
597+
const txBody: Cardano.TxBody = {
598+
fee: BigInt(0),
599+
inputs: [{}, {}, {}] as Cardano.TxIn[],
600+
outputs: []
601+
};
602+
603+
const scripts: Cardano.Script[] = [
604+
{
605+
__type: Cardano.ScriptType.Native,
606+
keyHash: Ed25519KeyHashHex(ownStakeKeyHash),
607+
kind: Cardano.NativeScriptKind.RequireSignature
608+
}
609+
];
610+
611+
expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, undefined, scripts)).toEqual([
612+
{
613+
index: 0,
614+
role: KeyRole.Stake
615+
}
616+
]);
617+
});
618+
619+
it('does not include derivation paths from native scripts with foreign key hashes', async () => {
620+
const txBody: Cardano.TxBody = {
621+
fee: BigInt(0),
622+
inputs: [{}, {}, {}] as Cardano.TxIn[],
623+
outputs: []
624+
};
625+
626+
const scripts: Cardano.Script[] = [
627+
{
628+
__type: Cardano.ScriptType.Native,
629+
keyHash: Ed25519KeyHashHex(otherStakeKeyHash),
630+
kind: Cardano.NativeScriptKind.RequireSignature
631+
}
632+
];
633+
634+
expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, undefined, scripts)).toEqual([]);
635+
});
636+
});
581637
});

packages/key-management/test/util/stubSignTransaction.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('KeyManagement.util.stubSignTransaction', () => {
3434
})
3535
).size
3636
).toBe(2);
37-
expect(ownSignatureKeyPaths).toBeCalledWith(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash);
37+
const expectedArgs = [txBody, knownAddresses, txInKeyPathMap, dRepKeyHash, undefined] as const;
38+
expect(ownSignatureKeyPaths).toBeCalledWith(...expectedArgs);
3839
});
3940
});

0 commit comments

Comments
 (0)