Skip to content

Commit 9286c0e

Browse files
fix: fix native script singing
1 parent 54c2440 commit 9286c0e

File tree

11 files changed

+389
-54
lines changed

11 files changed

+389
-54
lines changed

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

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as Crypto from '@cardano-sdk/crypto';
2-
import { AccountKeyDerivationPath, GroupedAddress, KeyRole, TxInId, TxInKeyPathMap } from '../types';
2+
import { AccountKeyDerivationPath, GroupedAddress, TxInId, TxInKeyPathMap } from '../types';
33
import { Cardano } from '@cardano-sdk/core';
44
import { DREP_KEY_DERIVATION_PATH } from './key';
5-
import { Ed25519KeyHashHex } from '@cardano-sdk/crypto';
65
import { isNotNil } from '@cardano-sdk/util';
76
import isEqual from 'lodash/isEqual.js';
87
import uniqBy from 'lodash/uniqBy.js';
@@ -167,7 +166,9 @@ export const checkStakeCredentialCertificates = (
167166
const getSignersData = (groupedAddresses: GroupedAddress[]): StakeKeySignerData[] =>
168167
uniqBy(groupedAddresses, 'rewardAccount')
169168
.map((groupedAddress) => {
170-
const stakeKeyHash = Cardano.RewardAccount.toHash(groupedAddress.rewardAccount) as unknown as Ed25519KeyHashHex;
169+
const stakeKeyHash = Cardano.RewardAccount.toHash(
170+
groupedAddress.rewardAccount
171+
) as unknown as Crypto.Ed25519KeyHashHex;
171172
const poolId = Cardano.PoolId.fromKeyHash(stakeKeyHash);
172173
return {
173174
derivationPath: groupedAddress.stakeKeyDerivationPath,
@@ -299,18 +300,13 @@ const checkStakeCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519Ke
299300

300301
const checkPaymentCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519KeyHashHex) => {
301302
const paymentCredential = Cardano.Address.fromBech32(address.address)?.asBase()?.getPaymentCredential();
302-
if (paymentCredential?.type === Cardano.CredentialType.ScriptHash && paymentCredential.hash === keyHash)
303+
if (paymentCredential?.hash === keyHash) {
303304
return {
304305
derivationPaths: [{ index: address.index, role: Number(address.type) }],
305306
requiresForeignSignatures: false
306307
};
307-
308-
if (paymentCredential?.type === Cardano.CredentialType.ScriptHash) {
309-
return {
310-
derivationPaths: [{ index: address.index, role: KeyRole.External }],
311-
requiresForeignSignatures: false
312-
};
313308
}
309+
314310
return { derivationPaths: [], requiresForeignSignatures: true };
315311
};
316312

@@ -327,7 +323,7 @@ const processSignatureScript = (
327323

328324
for (const address of groupedAddresses) {
329325
if (address.stakeKeyDerivationPath) {
330-
signatureCheck = checkStakeCredential(address, script.keyHash);
326+
signatureCheck = combineSignatureChecks(signatureCheck, checkStakeCredential(address, script.keyHash));
331327
}
332328
signatureCheck = combineSignatureChecks(signatureCheck, checkPaymentCredential(address, script.keyHash));
333329
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -634,10 +634,10 @@ describe('KeyManagement.util.ownSignaturePaths', () => {
634634
expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, undefined, scripts)).toEqual([]);
635635
});
636636
it('includes derivation paths for multi-signature native scripts', async () => {
637-
const scriptAddress = Cardano.PaymentAddress(
637+
const walletAddress = Cardano.PaymentAddress(
638638
'addr_test1xr806j8xcq6cw6jjkzfxyewyue33zwnu4ajnu28hakp5fmc6gddlgeqee97vwdeafwrdgrtzp2rw8rlchjf25ld7r2ssptq3m9'
639639
);
640-
const scriptRewardAccount = Cardano.RewardAccount(
640+
const walletRewardAccount = Cardano.RewardAccount(
641641
'stake_test17qdyxkl5vsvujlx8xu75hpk5p43q4phr3lutey420klp4gg7zmhrn'
642642
);
643643
const txBody: Cardano.TxBody = {
@@ -653,7 +653,7 @@ describe('KeyManagement.util.ownSignaturePaths', () => {
653653
scripts: [
654654
{
655655
__type: Cardano.ScriptType.Native,
656-
keyHash: Ed25519KeyHashHex('b498c0eaceb9a8c7c829d36fc84e892113c9d2636b53b0636d7518b4'),
656+
keyHash: Ed25519KeyHashHex('cefd48e6c035876a52b0926265c4e663113a7caf653e28f7ed8344ef'),
657657
kind: Cardano.NativeScriptKind.RequireSignature
658658
},
659659
{
@@ -665,7 +665,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => {
665665
}
666666
];
667667

668-
const knownAddress = createGroupedAddress(scriptAddress, scriptRewardAccount, AddressType.External, 0);
668+
const knownAddress = createGroupedAddress(walletAddress, walletRewardAccount, AddressType.External, 0);
669+
669670
expect(util.ownSignatureKeyPaths(txBody, [knownAddress], {}, undefined, scripts)).toEqual([
670671
{
671672
index: 0,

packages/wallet/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
module.exports = {
22
...require('../../test/jest.config'),
3-
setupFiles: ['jest-webextension-mock']
3+
setupFiles: ['jest-webextension-mock', './jest.setup.js']
44
};

packages/wallet/jest.setup.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Polyfill for Array.prototype.findLast on unit tests
2+
if (!Array.prototype.findLast) {
3+
// eslint-disable-next-line no-extend-native
4+
Array.prototype.findLast = function (predicate, thisArg) {
5+
if (!Array.isArray(this)) {
6+
throw new TypeError('Array.prototype.findLast called on non-array object');
7+
}
8+
if (typeof predicate !== 'function') {
9+
throw new TypeError('predicate must be a function');
10+
}
11+
12+
for (let i = this.length - 1; i >= 0; i--) {
13+
if (predicate.call(thisArg, this[i], i, this)) {
14+
return this[i];
15+
}
16+
}
17+
};
18+
}

packages/wallet/src/Wallets/BaseWallet.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,11 +668,16 @@ export class BaseWallet implements ObservableWallet {
668668
transaction = Serialization.Transaction.fromCbor(tx);
669669
}
670670

671+
const txWitness = transaction.witnessSet().toCore();
671672
const context = {
672673
...signingContext,
673674
dRepPublicKey,
674675
knownAddresses,
675-
txInKeyPathMap: await util.createTxInKeyPathMap(transaction.body().toCore(), knownAddresses, this.util)
676+
scripts: [...(signingContext?.scripts ?? []), ...(witness?.scripts ?? []), ...(txWitness?.scripts ?? [])],
677+
// Script wallets cant sign specific outputs with keys, the signatures are added to satisfy the witness script
678+
txInKeyPathMap: isBip32PublicCredentialsManager(this.#publicCredentialsManager)
679+
? await util.createTxInKeyPathMap(transaction.body().toCore(), knownAddresses, this.util)
680+
: {}
676681
};
677682

678683
const result = await this.witnesser.witness(transaction, context, signingOptions);

packages/wallet/src/cip30.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '@cardano-sdk/dapp-connector';
2121
import { Cardano, Milliseconds, Serialization, coalesceValueQuantities } from '@cardano-sdk/core';
2222
import { Ed25519KeyHashHex } from '@cardano-sdk/crypto';
23-
import { HexBlob, ManagedFreeableScope } from '@cardano-sdk/util';
23+
import { HexBlob } from '@cardano-sdk/util';
2424
import { InputSelectionError, InputSelectionFailure } from '@cardano-sdk/input-selection';
2525
import { Logger } from 'ts-log';
2626
import { MessageSender } from '@cardano-sdk/key-management';
@@ -370,23 +370,16 @@ const baseCip30WalletApi = (
370370
return addresses.map((groupAddresses) => cardanoAddressToCbor(groupAddresses.address));
371371
},
372372
getUtxos: async (_: SenderContext, amount?: Cbor, paginate?: Paginate): Promise<Cbor[] | null> => {
373-
const scope = new ManagedFreeableScope();
374-
try {
375-
const wallet = await firstValueFrom(wallet$);
376-
await waitForWalletStateSettle(wallet);
377-
let utxos = amount
378-
? await selectUtxo(wallet, parseValueCbor(amount).toCore(), !!paginate)
379-
: await firstValueFrom(wallet.utxo.available$);
380-
if (!utxos) return null;
381-
if (paginate) {
382-
utxos = utxos.slice(paginate.page * paginate.limit, paginate.page * paginate.limit + paginate.limit);
383-
}
384-
const cbor = utxos.map((core) => Serialization.TransactionUnspentOutput.fromCore(core).toCbor());
385-
scope.dispose();
386-
return cbor;
387-
} finally {
388-
scope.dispose();
373+
const wallet = await firstValueFrom(wallet$);
374+
await waitForWalletStateSettle(wallet);
375+
let utxos = amount
376+
? await selectUtxo(wallet, parseValueCbor(amount).toCore(), !!paginate)
377+
: await firstValueFrom(wallet.utxo.available$);
378+
if (!utxos) return null;
379+
if (paginate) {
380+
utxos = utxos.slice(paginate.page * paginate.limit, paginate.page * paginate.limit + paginate.limit);
389381
}
382+
return utxos.map((core) => Serialization.TransactionUnspentOutput.fromCore(core).toCbor());
390383
},
391384
signData: async (
392385
{ sender }: SenderContext,
@@ -424,7 +417,6 @@ const baseCip30WalletApi = (
424417
throw new DataSignError(DataSignErrorCode.UserDeclined, 'user declined signing');
425418
},
426419
signTx: async ({ sender }: SenderContext, tx: Cbor, partialSign?: Boolean): Promise<Cbor> => {
427-
const scope = new ManagedFreeableScope();
428420
logger.debug('signTx', tx);
429421
const txCbor = Serialization.TxCBOR(tx);
430422
const txDecoded = Serialization.Transaction.fromCbor(txCbor);
@@ -482,11 +474,8 @@ const baseCip30WalletApi = (
482474
const message = formatUnknownError(error);
483475
throw new TxSignError(TxSignErrorCode.UserDeclined, message);
484476
}
485-
} finally {
486-
scope.dispose();
487477
}
488478
} else {
489-
scope.dispose();
490479
throw new TxSignError(TxSignErrorCode.UserDeclined, 'user declined signing tx');
491480
}
492481
},

packages/wallet/test/PersonalWallet/methods.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
} from '@cardano-sdk/core';
2020
import { HexBlob } from '@cardano-sdk/util';
2121
import { InitializeTxProps } from '@cardano-sdk/tx-construction';
22+
import { LargeFirstSelector } from '@cardano-sdk/input-selection';
23+
import { MockChangeAddressResolver, getPayToPubKeyHashScript, getPaymentCredential } from '../hardware/utils';
2224
import { babbageTx } from '../../../core/test/Serialization/testData';
2325
import { buildDRepAddressFromDRepKey, toOutgoingTx, waitForWalletStateSettle } from '../util';
2426
import { getPassphrase, stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks';
@@ -321,6 +323,66 @@ describe('BaseWallet methods', () => {
321323
expect(tx.witness.signatures.size).toBe(2); // spending key and stake key for withdrawal
322324
});
323325

326+
it('can sign with native scripts credentials', async () => {
327+
const walletWithMockInputResolver = createPersonalWallet(
328+
{ name: 'Test Wallet' },
329+
{
330+
addressDiscovery,
331+
assetProvider,
332+
bip32Account,
333+
chainHistoryProvider,
334+
handleProvider,
335+
inputResolver: {
336+
resolveInput: jest.fn().mockResolvedValue(outputs[0])
337+
},
338+
logger,
339+
networkInfoProvider,
340+
rewardAccountInfoProvider,
341+
rewardsProvider,
342+
txSubmitProvider,
343+
utxoProvider,
344+
witnesser
345+
}
346+
);
347+
348+
const selector = new LargeFirstSelector({
349+
changeAddressResolver: new MockChangeAddressResolver()
350+
});
351+
walletWithMockInputResolver.setInputSelector(selector);
352+
const txBuilder = walletWithMockInputResolver.createTxBuilder();
353+
const firstAddress = (await firstValueFrom(walletWithMockInputResolver.addresses$))[0].address;
354+
355+
const builtTx = await txBuilder
356+
.addInput(
357+
[
358+
{
359+
address: Cardano.PaymentAddress('addr_test1vzztre5epvtj5p72sh28nvrs3e6s4xxn95f66cvg0sqsk7qd3mah0'),
360+
index: 0,
361+
txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5')
362+
},
363+
outputs[0]
364+
],
365+
{
366+
script: getPayToPubKeyHashScript(
367+
getPaymentCredential(firstAddress).hash as unknown as Crypto.Ed25519KeyHashHex
368+
)
369+
}
370+
)
371+
.addOutput(txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(5_111_111n)).toTxOut())
372+
.customize(({ txBody }) => ({
373+
...txBody,
374+
withdrawals: []
375+
}))
376+
.build()
377+
.inspect();
378+
379+
const {
380+
witness: { signatures }
381+
} = await walletWithMockInputResolver.finalizeTx({ tx: builtTx, witness: builtTx.witness });
382+
383+
expect(signatures.size).toBe(1);
384+
});
385+
324386
it('passes through sender to witnesser', async () => {
325387
const sender = { url: 'https://lace.io' };
326388
const txInternals = await wallet.initializeTx(props);

packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ import { HID } from 'node-hid';
1616
import { Hash32ByteBase16 } from '@cardano-sdk/crypto';
1717
import { HexBlob } from '@cardano-sdk/util';
1818
import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction';
19+
import { LargeFirstSelector } from '@cardano-sdk/input-selection';
1920
import { LedgerKeyAgent, LedgerTransportType } from '@cardano-sdk/hardware-ledger';
21+
import {
22+
MockChangeAddressResolver,
23+
getPayToPubKeyHashScript,
24+
getPaymentCredential,
25+
getStakeCredential
26+
} from '../utils';
2027
import { buildDRepAddressFromDRepKey } from '../../util';
2128
import { firstValueFrom } from 'rxjs';
2229
import { getDevices } from '@ledgerhq/hw-transport-node-hid-noevents';
@@ -40,14 +47,6 @@ const cleanupEstablishedConnections = async () => {
4047
LedgerKeyAgent.deviceConnections = [];
4148
};
4249

43-
const getStakeCredential = (rewardAccount: Cardano.RewardAccount) => {
44-
const stakeKeyHash = Cardano.RewardAccount.toHash(rewardAccount);
45-
return {
46-
hash: stakeKeyHash,
47-
type: Cardano.CredentialType.KeyHash
48-
};
49-
};
50-
5150
const signAndDecode = async (signWith: Cardano.PaymentAddress | Cardano.RewardAccount, wallet: BaseWallet) => {
5251
const dataSignature = await wallet.signData({
5352
payload: HexBlob('abc123'),
@@ -357,6 +356,86 @@ describe('LedgerKeyAgent', () => {
357356
expect(signatures.size).toBe(3);
358357
});
359358

359+
describe('Native Scripts', () => {
360+
it('can sign transaction with native script - Payment credential', async () => {
361+
const selector = new LargeFirstSelector({
362+
changeAddressResolver: new MockChangeAddressResolver()
363+
});
364+
wallet.setInputSelector(selector);
365+
const txBuilder = wallet.createTxBuilder();
366+
const firstAddress = (await firstValueFrom(wallet.addresses$))[0].address;
367+
368+
const builtTx = await txBuilder
369+
.addInput(
370+
[
371+
{
372+
address: Cardano.PaymentAddress('addr_test1vzztre5epvtj5p72sh28nvrs3e6s4xxn95f66cvg0sqsk7qd3mah0'),
373+
index: 0,
374+
txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5')
375+
},
376+
outputs[0]
377+
],
378+
{
379+
script: getPayToPubKeyHashScript(
380+
getPaymentCredential(firstAddress).hash as unknown as Crypto.Ed25519KeyHashHex
381+
)
382+
}
383+
)
384+
.addOutput(txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(5_111_111n)).toTxOut())
385+
.customize(({ txBody }) => ({
386+
...txBody,
387+
withdrawals: []
388+
}))
389+
.build()
390+
.inspect();
391+
392+
const {
393+
witness: { signatures }
394+
} = await wallet.finalizeTx({ tx: builtTx, witness: builtTx.witness });
395+
396+
expect(signatures.size).toBe(1);
397+
});
398+
399+
it('can sign transaction with native script - Stake credential', async () => {
400+
const selector = new LargeFirstSelector({
401+
changeAddressResolver: new MockChangeAddressResolver()
402+
});
403+
wallet.setInputSelector(selector);
404+
const txBuilder = wallet.createTxBuilder();
405+
406+
const builtTx = await txBuilder
407+
.addInput(
408+
[
409+
{
410+
address: Cardano.PaymentAddress('addr_test1vzztre5epvtj5p72sh28nvrs3e6s4xxn95f66cvg0sqsk7qd3mah0'),
411+
index: 0,
412+
txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5')
413+
},
414+
outputs[0]
415+
],
416+
{
417+
script: getPayToPubKeyHashScript(
418+
getStakeCredential((await firstValueFrom(wallet.delegation.rewardAccounts$))?.[0].address)
419+
.hash as unknown as Crypto.Ed25519KeyHashHex
420+
)
421+
}
422+
)
423+
.addOutput(txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(5_111_111n)).toTxOut())
424+
.customize(({ txBody }) => ({
425+
...txBody,
426+
withdrawals: []
427+
}))
428+
.build()
429+
.inspect();
430+
431+
const {
432+
witness: { signatures }
433+
} = await wallet.finalizeTx({ tx: builtTx, witness: builtTx.witness });
434+
435+
expect(signatures.size).toBe(1);
436+
});
437+
});
438+
360439
describe('conway-era', () => {
361440
describe('ordinary tx mode', () => {
362441
let dRepPublicKey: Crypto.Ed25519PublicKeyHex | undefined;

0 commit comments

Comments
 (0)