From 838edbe1c7b2c58d17426a19fa5f36465e383aea Mon Sep 17 00:00:00 2001 From: Rob Woodgate Date: Tue, 11 Aug 2026 21:49:47 +0100 Subject: [PATCH 1/2] refactor(sigall): recompute signing digests from package contents (#947) (cherry picked from commit 20a29e2032c707ba0d4f0b6db342acab5df78e39) --- etc/cashu-ts.api.md | 13 +-- src/crypto/NUT11.ts | 38 +----- src/model/SigAll.ts | 131 +++++++-------------- src/wallet/Wallet.ts | 12 +- test/crypto/NUT11.test.ts | 95 +++++++-------- test/model/SigAll.test.ts | 240 ++++++++++++++++++-------------------- 6 files changed, 206 insertions(+), 323 deletions(-) diff --git a/etc/cashu-ts.api.md b/etc/cashu-ts.api.md index a60ec0cad..c66cf921c 100644 --- a/etc/cashu-ts.api.md +++ b/etc/cashu-ts.api.md @@ -1885,19 +1885,16 @@ export type SigAllApi = { extractSwapPackage: (preview: SwapPreview) => SigAllSigningPackage; extractMeltPackage: >(preview: MeltPreview) => SigAllSigningPackage; serializePackage: (pkg: SigAllSigningPackage) => string; - deserializePackage: (input: string, options?: { - validateDigest?: boolean; - }) => SigAllSigningPackage; + deserializePackage: (input: string) => SigAllSigningPackage; signPackage: (pkg: SigAllSigningPackage, privkey: string) => SigAllSigningPackage; signDigest: (hexDigest: string, privkey: string) => string; mergeSwapPackage: (pkg: SigAllSigningPackage, preview: SwapPreview) => SwapPreview; mergeMeltPackage: >(pkg: SigAllSigningPackage, preview: MeltPreview) => MeltPreview; }; -// @public (undocumented) +// @public export type SigAllDigests = { - legacy: string; - current: string; + v0: string; }; // @public @@ -1907,10 +1904,6 @@ export type SigAllSigningPackage = { quote?: string; inputs: Array>; outputs: SerializedBlindedMessage[]; - digests: { - legacy?: string; - current: string; - }; witness?: { signatures: string[]; }; diff --git a/src/crypto/NUT11.ts b/src/crypto/NUT11.ts index 4c2498337..1157ab93e 100644 --- a/src/crypto/NUT11.ts +++ b/src/crypto/NUT11.ts @@ -681,7 +681,7 @@ export function assertSigAllInputs(inputs: Proof[]): void { } /** - * Message aggregation for SIG_ALL. + * Message aggregation for SIG_ALL (v0, unframed concatenation). * * NOTE: Use `assertSigAllInputs()` to ensure valid message inputs. * @@ -692,7 +692,7 @@ export function assertSigAllInputs(inputs: Proof[]): void { * @param quoteId Optional. Quote id for Melt transactions. * @internal */ -export function buildP2PKSigAllMessage( +export function buildP2PKSigAllMessageV0( inputs: Array>, outputs: Array>, quoteId?: string, @@ -856,37 +856,3 @@ function resolveNSigsRefund(secret: Secret, lockState: LockState, refundKeys: st } return 0; // refund lock inactive } - -// ------------------------------ -// Deprecated -// ------------------------------ - -/** - * Message aggregation for SIG_ALL (legacy format). - * - * @remarks - * Melt transactions MUST include the quoteId. - * - * For compatibility with NutShell (all releases), CDK >, - outputs: Array>, - quoteId?: string, -): string { - const parts: string[] = []; - // Concat inputs: secret_0 ... - for (const p of inputs) { - parts.push(p.secret); - } - // Concat outputs: B_0 ... - for (const o of outputs) { - parts.push(o.blindedMessage.B_); - } - // Add quoteId for melts - if (quoteId) { - parts.push(quoteId); - } - return parts.join(''); -} diff --git a/src/model/SigAll.ts b/src/model/SigAll.ts index 17fcf5293..26a56d82b 100644 --- a/src/model/SigAll.ts +++ b/src/model/SigAll.ts @@ -1,9 +1,4 @@ -import { - computeMessageDigest, - buildLegacyP2PKSigAllMessage, - buildP2PKSigAllMessage, - schnorrSignDigest, -} from '../crypto'; +import { computeMessageDigest, buildP2PKSigAllMessageV0, schnorrSignDigest } from '../crypto'; import { parseWitnessData } from '../crypto/NUT11'; import { Bytes, JSONInt, encodeUint8toBase64Url } from '../utils'; import type { MeltPreview, SwapPreview } from '../wallet/types'; @@ -18,11 +13,15 @@ import type { Proof, MeltQuoteBaseResponse, SerializedBlindedMessage } from './t const SIGALL_PREFIX = 'sigallA'; /** + * Per-format SIG_ALL digests, keyed by transcript version. + * * @experimental */ export type SigAllDigests = { - legacy: string; - current: string; + /** + * Unframed concatenation format (CDK >= 0.14.0, Nutshell > 0.20.2). + */ + v0: string; }; /** @@ -43,7 +42,7 @@ export type SigAllSigningPackage = { */ type: 'swap' | 'melt'; /** - * For melt packages only. + * Required for melt packages, absent for swaps. */ quote?: string; /** @@ -54,19 +53,6 @@ export type SigAllSigningPackage = { * NUT-00 `BlindedMessages` for signing verification. */ outputs: SerializedBlindedMessage[]; - /** - * Per-format digests to support multiple SIG_ALL formats. - */ - digests: { - /** - * For Nutshell (all releases), CDK < 0.14.0. - */ - legacy?: string; - /** - * From CDK >= 0.14.0. - */ - current: string; - }; /** * Signatures collected (to be injected into the first proof witness). */ @@ -79,12 +65,10 @@ function computeDigests( quoteId?: string, ): SigAllDigests { const sigAllOutputs = outputs.map((blindedMessage) => ({ blindedMessage })); - const legacyMsg = buildLegacyP2PKSigAllMessage(inputs, sigAllOutputs, quoteId); - const currentMsg = buildP2PKSigAllMessage(inputs, sigAllOutputs, quoteId); + const v0Msg = buildP2PKSigAllMessageV0(inputs, sigAllOutputs, quoteId); return { - legacy: computeMessageDigest(legacyMsg, true), - current: computeMessageDigest(currentMsg, true), + v0: computeMessageDigest(v0Msg, true), }; } @@ -97,7 +81,6 @@ function serializePackage(pkg: SigAllSigningPackage): string { ordered.inputs = pkg.inputs; ordered.outputs = pkg.outputs; - if (pkg.digests) ordered.digests = pkg.digests; if (pkg.witness) ordered.witness = pkg.witness; const json = JSONInt.stringify(ordered) ?? '{}'; @@ -106,10 +89,7 @@ function serializePackage(pkg: SigAllSigningPackage): string { return `${SIGALL_PREFIX}${base64url}`; } -function deserializePackage( - input: string, - options?: { validateDigest?: boolean }, -): SigAllSigningPackage { +function deserializePackage(input: string): SigAllSigningPackage { if (!input.startsWith(SIGALL_PREFIX)) { throw new CTSError(`Invalid signing package: must start with "${SIGALL_PREFIX}"`); } @@ -153,6 +133,12 @@ function deserializePackage( throw new CTSError(`Invalid signing package type: ${type}`); } + // The quote is part of the signed melt transcript; without it a melt package + // would produce a swap-shaped message. + if (type === 'melt' && (typeof pkg.quote !== 'string' || pkg.quote.length === 0)) { + throw new CTSError('Melt signing package requires a quote'); + } + if (!Array.isArray(pkg.inputs)) { throw new CTSError('Signing package inputs must be an array'); } @@ -188,42 +174,34 @@ function deserializePackage( output.amount = Amount.from(output.amount); } - const digests = pkg.digests as Record | undefined; - if (!digests || typeof digests.current !== 'string' || digests.current.length === 0) { - throw new CTSError('Signing package digests.current is required'); - } - - // Optional digest validation - if (options?.validateDigest) { - const recomputed = computeDigests(pkg.inputs, pkg.outputs, pkg.quote); - if (recomputed.current !== digests.current) { - throw new CTSError('Digest validation failed: current digest mismatch'); - } - if (digests.legacy && recomputed.legacy !== digests.legacy) { - throw new CTSError('Digest validation failed: legacy digest mismatch'); + if (pkg.witness !== undefined) { + const witness = pkg.witness as { signatures?: unknown }; + if ( + !witness || + typeof witness !== 'object' || + !Array.isArray(witness.signatures) || + witness.signatures.some((s) => typeof s !== 'string') + ) { + throw new CTSError('Signing package witness.signatures must be a string array'); } } - return pkg; + // Rebuild from validated fields only, so unknown keys never survive transport. + return { + version: SIGALL_PREFIX, + type, + ...(type === 'melt' ? { quote: pkg.quote } : {}), + inputs: pkg.inputs.map((p) => ({ secret: p.secret, C: p.C })), + outputs: pkg.outputs.map((o) => ({ amount: o.amount, id: o.id, B_: o.B_ })), + ...(pkg.witness ? { witness: { signatures: pkg.witness.signatures } } : {}), + }; } function signPackage(pkg: SigAllSigningPackage, privkey: string): SigAllSigningPackage { - const newSigs: string[] = []; - - if (!pkg.digests?.current) { - throw new CTSError('digests.current is required to sign package'); - } - - // Sign precomputed digests - newSigs.push(schnorrSignDigest(pkg.digests.current, privkey)); - if (pkg.digests.legacy) { - newSigs.push(schnorrSignDigest(pkg.digests.legacy, privkey)); - } - - // validate that signing actually produced signatures - if (newSigs.length === 0) { - throw new CTSError('No signatures produced during signing'); - } + // Sign transcripts recomputed from the package contents; a signer only ever + // signs what the package shows, never a digest chosen elsewhere. + const digests = computeDigests(pkg.inputs, pkg.outputs, pkg.quote); + const newSigs = [schnorrSignDigest(digests.v0, privkey)]; return { ...pkg, @@ -258,27 +236,12 @@ function buildSigningPackage( outputs: SerializedBlindedMessage[], quoteId?: string, ): SigAllSigningPackage { - // compute legacy and current SIG_ALL digests for backward compatibility - const digests = computeDigests(inputs, outputs, quoteId); - - // verify current digest was computed correctly (catches bugs). - const sigAllOutputs = outputs.map((blindedMessage) => ({ blindedMessage })); - const msg = buildP2PKSigAllMessage(inputs, sigAllOutputs, quoteId); - const expected = computeMessageDigest(msg, true); - - if (digests.current !== expected) { - throw new CTSError( - 'SIG_ALL digest computation mismatch - current digest does not match expected value', - ); - } - return { version: SIGALL_PREFIX, type, ...(quoteId ? { quote: quoteId } : {}), inputs: inputs.map((p) => ({ secret: p.secret, C: p.C })), outputs, - digests, }; } @@ -323,14 +286,12 @@ function mergeSignatures(proofs: Proof[], pkg: SigAllSigningPackage): Proof[] { */ export type SigAllApi = { /** - * Computes legacy and current SIG_ALL formats. + * Computes the SIG_ALL digests for a transaction, keyed by transcript version. * - * @remarks - * Returns hex-encoded SHA256 digests for each format to support multi-format signing. * @param inputs Proof array. * @param outputs Array of SerializedBlindMessage (NUT-00 `BlindMessages`). * @param quoteId Optional quote ID for melt transactions. - * @returns Object with legacy, and current digests (all hex strings) + * @returns Hex-encoded SHA256 digest per format. * @experimental */ computeDigests: ( @@ -379,19 +340,17 @@ export type SigAllApi = { /** * @remarks - * Accepts a sigallA-prefixed base64url string and rehydrates it into a SigAllSigningPackage. + * Accepts a sigallA-prefixed base64url string and rehydrates it into a SigAllSigningPackage. Only + * known fields survive the round trip. * @experimental */ - deserializePackage: ( - input: string, - options?: { validateDigest?: boolean }, - ) => SigAllSigningPackage; + deserializePackage: (input: string) => SigAllSigningPackage; /** * Signs a SigAllSigningPackage and returns it with signatures attached. * * @remarks - * Collects signatures by signing legacy and current SIG_ALL formats for backward compatibility. + * Signs the SIG_ALL transcripts recomputed from the package's own inputs, outputs and quote. * Multiple parties can call this sequentially to aggregate signatures for multi-party signing. * @param pkg The signing package (from extract*SigningPackage or another signer) * @param privkey Private key to sign with. diff --git a/src/wallet/Wallet.ts b/src/wallet/Wallet.ts index 17a13a39d..ad8e92c88 100644 --- a/src/wallet/Wallet.ts +++ b/src/wallet/Wallet.ts @@ -12,9 +12,8 @@ import { signP2PKProofs as cryptoSignP2PKProofs, hashToCurve, isP2PKSigAll, - buildP2PKSigAllMessage, + buildP2PKSigAllMessageV0, assertSigAllInputs, - buildLegacyP2PKSigAllMessage, parseSecret, } from '../crypto'; import { signMintQuoteAmended } from '../crypto/NUT20'; @@ -1483,14 +1482,11 @@ class Wallet { this.failIfNullish(outputData, 'OutputData is required for SIG_ALL proof signing.'); assertSigAllInputs(normalizedProofs); - // SIG_ALL is in flux currently, so let's generate all known message formats - // and sign the first proof only against each message... + // SIG_ALL is in flux currently, so sign the first proof only against each + // supported message format... const [first, ...rest] = normalizedProofs; let signedFirst = first; - const messages = [ - buildLegacyP2PKSigAllMessage(normalizedProofs, outputData, quoteId), - buildP2PKSigAllMessage(normalizedProofs, outputData, quoteId), - ]; + const messages = [buildP2PKSigAllMessageV0(normalizedProofs, outputData, quoteId)]; for (const msg of messages) { signedFirst = cryptoSignP2PKProofs([signedFirst], privkey, this._logger, msg)[0]; } diff --git a/test/crypto/NUT11.test.ts b/test/crypto/NUT11.test.ts index ed9baf2b7..b906f174b 100644 --- a/test/crypto/NUT11.test.ts +++ b/test/crypto/NUT11.test.ts @@ -21,9 +21,8 @@ import { schnorrVerifyMessage, deriveP2BKBlindedPubkeys, P2BK_DST, - buildP2PKSigAllMessage, + buildP2PKSigAllMessageV0, assertSigAllInputs, - buildLegacyP2PKSigAllMessage, createSecret, dedupeP2PKPubkeys, isHTLCSpendAuthorised, @@ -839,7 +838,7 @@ describe('schnorrVerifyMessage & hasP2PKSignedProof', () => { }); }); -describe('buildP2PKSigAllMessage, SIG_ALL aggregation', () => { +describe('buildP2PKSigAllMessageV0, SIG_ALL aggregation', () => { // Helpers const mkProof = (secret: string, C: string) => ({ secret, C }) as any; // keep minimal shape for this unit @@ -850,7 +849,7 @@ describe('buildP2PKSigAllMessage, SIG_ALL aggregation', () => { const inputs = [mkProof('sA', 'CA'), mkProof('sB', 'CB')]; const outputs = [mkOutput(2, 'B2'), mkOutput(5, 'B5')]; - const msg = buildP2PKSigAllMessage(inputs, outputs); + const msg = buildP2PKSigAllMessageV0(inputs, outputs); // manual expectation, inputs first, then outputs, no separators const expected = ['sA', 'CA', 'sB', 'CB', '2', 'B2', '5', 'B5'].join(''); @@ -861,8 +860,8 @@ describe('buildP2PKSigAllMessage, SIG_ALL aggregation', () => { const inputs = [mkProof('s1', 'C1')]; const outputs = [mkOutput(1, 'B1')]; - const msgNoQuote = buildP2PKSigAllMessage(inputs, outputs); - const msgWithQuote = buildP2PKSigAllMessage(inputs, outputs, 'quote-xyz'); + const msgNoQuote = buildP2PKSigAllMessageV0(inputs, outputs); + const msgWithQuote = buildP2PKSigAllMessageV0(inputs, outputs, 'quote-xyz'); expect(msgWithQuote).toBe(msgNoQuote + 'quote-xyz'); expect(msgWithQuote).not.toBe(msgNoQuote); @@ -873,8 +872,8 @@ describe('buildP2PKSigAllMessage, SIG_ALL aggregation', () => { const outNum = [mkOutput(7, 'B7')]; const outStr = [mkOutput('7', 'B7')]; - const mNum = buildP2PKSigAllMessage(inputs, outNum); - const mStr = buildP2PKSigAllMessage(inputs, outStr); + const mNum = buildP2PKSigAllMessageV0(inputs, outNum); + const mStr = buildP2PKSigAllMessageV0(inputs, outStr); expect(mNum).toBe(mStr); }); @@ -883,9 +882,9 @@ describe('buildP2PKSigAllMessage, SIG_ALL aggregation', () => { const baseInputs = [mkProof('s1', 'C1')]; const outputs = [mkOutput(3, 'B3')]; - const m1 = buildP2PKSigAllMessage(baseInputs, outputs); - const m2 = buildP2PKSigAllMessage([mkProof('s2', 'C1')], outputs); - const m3 = buildP2PKSigAllMessage([mkProof('s1', 'C2')], outputs); + const m1 = buildP2PKSigAllMessageV0(baseInputs, outputs); + const m2 = buildP2PKSigAllMessageV0([mkProof('s2', 'C1')], outputs); + const m3 = buildP2PKSigAllMessageV0([mkProof('s1', 'C2')], outputs); expect(m2).not.toBe(m1); expect(m3).not.toBe(m1); @@ -893,9 +892,9 @@ describe('buildP2PKSigAllMessage, SIG_ALL aggregation', () => { test('changing any output field changes the message', () => { const inputs = [mkProof('s1', 'C1')]; - const m1 = buildP2PKSigAllMessage(inputs, [mkOutput(3, 'B3')]); - const m2 = buildP2PKSigAllMessage(inputs, [mkOutput(4, 'B3')]); // amount changed - const m3 = buildP2PKSigAllMessage(inputs, [mkOutput(3, 'B4')]); // B_ changed + const m1 = buildP2PKSigAllMessageV0(inputs, [mkOutput(3, 'B3')]); + const m2 = buildP2PKSigAllMessageV0(inputs, [mkOutput(4, 'B3')]); // amount changed + const m3 = buildP2PKSigAllMessageV0(inputs, [mkOutput(3, 'B4')]); // B_ changed expect(m2).not.toBe(m1); expect(m3).not.toBe(m1); @@ -906,8 +905,8 @@ describe('buildP2PKSigAllMessage, SIG_ALL aggregation', () => { const inputsB = [...inputsA].reverse(); const outputs = [mkOutput(1, 'B1')]; - const mA = buildP2PKSigAllMessage(inputsA, outputs); - const mB = buildP2PKSigAllMessage(inputsB, outputs); + const mA = buildP2PKSigAllMessageV0(inputsA, outputs); + const mB = buildP2PKSigAllMessageV0(inputsB, outputs); expect(mA).not.toBe(mB); }); @@ -917,15 +916,15 @@ describe('buildP2PKSigAllMessage, SIG_ALL aggregation', () => { const outputsA = [mkOutput(1, 'B1'), mkOutput(2, 'B2')]; const outputsB = [...outputsA].reverse(); - const mA = buildP2PKSigAllMessage(inputs, outputsA); - const mB = buildP2PKSigAllMessage(inputs, outputsB); + const mA = buildP2PKSigAllMessageV0(inputs, outputsA); + const mB = buildP2PKSigAllMessageV0(inputs, outputsB); expect(mA).not.toBe(mB); }); test('empty arrays are allowed, quoteId only contributes when present', () => { - const mNone = buildP2PKSigAllMessage([], []); - const mQuoteOnly = buildP2PKSigAllMessage([], [], 'q123'); + const mNone = buildP2PKSigAllMessageV0([], []); + const mQuoteOnly = buildP2PKSigAllMessageV0([], [], 'q123'); expect(mNone).toBe(''); expect(mQuoteOnly).toBe('q123'); @@ -936,8 +935,8 @@ describe('buildP2PKSigAllMessage, SIG_ALL aggregation', () => { const outputs = [mkOutput(9, 'B9')]; const q = 'q999'; - const m1 = buildP2PKSigAllMessage(inputs, outputs, q); - const m2 = buildP2PKSigAllMessage(inputs, outputs, q); + const m1 = buildP2PKSigAllMessageV0(inputs, outputs, q); + const m2 = buildP2PKSigAllMessageV0(inputs, outputs, q); expect(m1).toBe(m2); }); @@ -1052,8 +1051,8 @@ describe('branch coverage helpers', () => { }); }); -describe('SIG_ALL, both message formats are actually signed', () => { - test('first proof witness contains signatures for legacy and final SIG_ALL messages', () => { +describe('SIG_ALL, the supported message format is actually signed', () => { + test('first proof witness contains a signature for the v0 SIG_ALL message', () => { // 1. Set up a keypair and a SIG_ALL P2PK secret const privBytes = schnorr.utils.randomSecretKey(); const privHex = bytesToHex(privBytes); @@ -1090,15 +1089,11 @@ describe('SIG_ALL, both message formats are actually signed', () => { const quoteId = 'quote-xyz'; - // 4. Build the three distinct SIG_ALL messages the wallet is supposed to sign - const legacyMsg = buildLegacyP2PKSigAllMessage(proofs, outputs, quoteId); - const finalMsg = buildP2PKSigAllMessage(proofs, outputs, quoteId); + // 4. Build the SIG_ALL messages the wallet is supposed to sign + const messages = [buildP2PKSigAllMessageV0(proofs, outputs, quoteId)]; - const messages = [legacyMsg, finalMsg]; - - // 5. Mimic the wallet SIG_ALL path: - // start from the first proof, then sign it three times with the three messages, - // threading the witness through on each call. + // 5. Mimic the wallet SIG_ALL path: start from the first proof, then sign it + // once per message, threading the witness through on each call. let signedFirst: Proof = proofs[0]; for (const msg of messages) { @@ -1107,8 +1102,8 @@ describe('SIG_ALL, both message formats are actually signed', () => { const sigs = getP2PKWitnessSignatures(signedFirst.witness); - // Sanity: we really appended two signatures - expect(sigs.length).toBe(2); + // Sanity: we appended one signature per supported format + expect(sigs.length).toBe(messages.length); // 6. For each message variant, there must be at least one signature that verifies // against that specific message and this pubkey. @@ -1250,7 +1245,7 @@ describe('NUT-11 test vectors', () => { 2, '038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39', ); - expect(buildP2PKSigAllMessage(proof, [outputs])).toEqual( + expect(buildP2PKSigAllMessageV0(proof, [outputs])).toEqual( '["P2PK",{"nonce":"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303","data":"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1","tags":[["sigflag","SIG_ALL"]]}]02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd2038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39', ); }); @@ -1270,7 +1265,7 @@ describe('NUT-11 test vectors', () => { '038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39', ); expect(() => assertSigAllInputs([proof])).not.toThrow(); - const mts = buildP2PKSigAllMessage([proof], [outputs]); + const mts = buildP2PKSigAllMessageV0([proof], [outputs]); expect(isP2PKSpendAuthorised(proof, NULL_LOGGER, mts)).toBe(true); }); @@ -1300,7 +1295,7 @@ describe('NUT-11 test vectors', () => { ]; // The assert catches the error. The signature would otherwise be valid expect(() => assertSigAllInputs(proofs)).toThrow(/must share identical Secret\.tags/); - const mts = buildP2PKSigAllMessage(proofs, outputs); + const mts = buildP2PKSigAllMessageV0(proofs, outputs); expect(isP2PKSpendAuthorised(proofs[0], NULL_LOGGER, mts)).toBe(true); }); @@ -1321,7 +1316,7 @@ describe('NUT-11 test vectors', () => { ]; // The assert catches the error. The signature would otherwise be valid expect(() => assertSigAllInputs(proofs)).not.toThrow(); - const mts = buildP2PKSigAllMessage(proofs, outputs); + const mts = buildP2PKSigAllMessageV0(proofs, outputs); expect(isP2PKSpendAuthorised(proofs[0], NULL_LOGGER, mts)).toBe(true); }); @@ -1341,7 +1336,7 @@ describe('NUT-11 test vectors', () => { mkOutput(2, '038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39'), ]; expect(() => assertSigAllInputs(proofs)).not.toThrow(); - const mts = buildP2PKSigAllMessage(proofs, outputs); + const mts = buildP2PKSigAllMessageV0(proofs, outputs); expect(isP2PKSpendAuthorised(proofs[0], NULL_LOGGER, mts)).toBe(true); }); @@ -1361,7 +1356,7 @@ describe('NUT-11 test vectors', () => { mkOutput(2, '038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39'), ]; expect(() => assertSigAllInputs(proofs)).not.toThrow(); - const mts = buildP2PKSigAllMessage(proofs, outputs); + const mts = buildP2PKSigAllMessageV0(proofs, outputs); expect(isHTLCSpendAuthorised(proofs[0], NULL_LOGGER, mts)).toBe(true); }); @@ -1382,7 +1377,7 @@ describe('NUT-11 test vectors', () => { mkOutput(1, '03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5'), ]; expect(() => assertSigAllInputs(proofs)).not.toThrow(); - const mts = buildP2PKSigAllMessage(proofs, outputs); + const mts = buildP2PKSigAllMessageV0(proofs, outputs); expect(isHTLCSpendAuthorised(proofs[0], NULL_LOGGER, mts)).toBe(false); }); @@ -1402,7 +1397,7 @@ describe('NUT-11 test vectors', () => { mkOutput(2, '038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39'), ]; expect(() => assertSigAllInputs(proofs)).not.toThrow(); - const mts = buildP2PKSigAllMessage(proofs, outputs); + const mts = buildP2PKSigAllMessageV0(proofs, outputs); expect(isHTLCSpendAuthorised(proofs[0], NULL_LOGGER, mts)).toBe(true); }); @@ -1423,7 +1418,7 @@ describe('NUT-11 test vectors', () => { const outputs = [ mkOutput(0, '038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39'), ]; - const message = buildP2PKSigAllMessage(inputs, outputs, quote); + const message = buildP2PKSigAllMessageV0(inputs, outputs, quote); expect(message).toBe( '["P2PK",{"nonce":"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de","data":"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835","tags":[["sigflag","SIG_ALL"]]}]02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b0038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0', @@ -1448,7 +1443,7 @@ describe('NUT-11 test vectors', () => { ]; const quote = 'cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0'; expect(() => assertSigAllInputs(proofs)).not.toThrow(); - const mts = buildP2PKSigAllMessage(proofs, outputs, quote); + const mts = buildP2PKSigAllMessageV0(proofs, outputs, quote); expect(isHTLCSpendAuthorised(proofs[0], NULL_LOGGER, mts)).toBe(true); }); @@ -1469,7 +1464,7 @@ describe('NUT-11 test vectors', () => { ]; const quote = 'Db3qEMVwFN2tf_1JxbZp29aL5cVXpSMIwpYfyOVF'; expect(() => assertSigAllInputs(proofs)).not.toThrow(); - const mts = buildP2PKSigAllMessage(proofs, outputs, quote); + const mts = buildP2PKSigAllMessageV0(proofs, outputs, quote); expect(isHTLCSpendAuthorised(proofs[0], NULL_LOGGER, mts)).toBe(true); }); }); @@ -2126,14 +2121,4 @@ describe('SIG_ALL edge cases', () => { expect(isP2PKSigAll([sigInputs])).toBe(false); expect(isP2PKSigAll([])).toBe(false); }); - - test('buildLegacyP2PKSigAllMessage concatenates secrets, outputs, then quoteId', () => { - const inputs = [{ secret: 's1' }, { secret: 's2' }]; - const outputs = [ - { blindedMessage: { amount: 1, B_: 'B1' } }, - { blindedMessage: { amount: 2, B_: 'B2' } }, - ] as any; - expect(buildLegacyP2PKSigAllMessage(inputs, outputs, 'q1')).toBe('s1s2B1B2q1'); - expect(buildLegacyP2PKSigAllMessage(inputs, outputs)).toBe('s1s2B1B2'); - }); }); diff --git a/test/model/SigAll.test.ts b/test/model/SigAll.test.ts index 6dc15f1f0..a2cc9ce9c 100644 --- a/test/model/SigAll.test.ts +++ b/test/model/SigAll.test.ts @@ -5,7 +5,10 @@ import { type SigAllSigningPackage, MeltQuoteState, Amount, + computeMessageDigest, CTSError, + getPubKeyFromPrivKey, + schnorrVerifyDigest, type OutputDataLike, type Proof, type P2PKWitness, @@ -13,6 +16,7 @@ import { type MeltPreview, type SwapPreview, } from '../../src'; +import { Bytes } from '../../src/utils'; const dummyProof: Proof = { id: 'testid', @@ -86,21 +90,14 @@ function decodeRawJson(input: string): string { describe('SigAll — computeDigests', () => { test('produces hex strings of correct length', () => { const digests = SigAll.computeDigests([dummyProof], [dummyBlindedMessage], 'dummyquote'); - expect(typeof digests.legacy).toBe('string'); - expect(typeof digests.current).toBe('string'); - expect(digests.legacy.length).toBe(64); - expect(digests.current.length).toBe(64); - }); - - test('legacy and current digests differ', () => { - const digests = SigAll.computeDigests([dummyProof], [dummyBlindedMessage]); - expect(digests.legacy).not.toBe(digests.current); + expect(typeof digests.v0).toBe('string'); + expect(digests.v0.length).toBe(64); }); test('quoteId changes the digests', () => { const without = SigAll.computeDigests([dummyProof], [dummyBlindedMessage]); const with_ = SigAll.computeDigests([dummyProof], [dummyBlindedMessage], 'somequote'); - expect(without.current).not.toBe(with_.current); + expect(without.v0).not.toBe(with_.v0); }); }); @@ -112,8 +109,6 @@ describe('SigAll — extractSwapPackage', () => { expect(pkg.quote).toBeUndefined(); expect(pkg.inputs.length).toBe(1); expect(pkg.outputs.length).toBe(2); // keepOutputs + sendOutputs - expect(pkg.digests.current.length).toBe(64); - expect(pkg.digests.legacy!.length).toBe(64); }); test('merges keepOutputs and sendOutputs in order', () => { @@ -144,7 +139,6 @@ describe('SigAll — extractMeltPackage', () => { expect(pkg.quote).toBe('dummyquote'); expect(pkg.inputs.length).toBe(1); expect(pkg.outputs.length).toBe(1); - expect(pkg.digests.current.length).toBe(64); }); test('includes quote id', () => { @@ -160,7 +154,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [{ secret: 'testsecret', C: '02' + '1'.repeat(64) }], outputs: [dummyBlindedMessage], - digests: SigAll.computeDigests([dummyProof], [dummyBlindedMessage]), witness: { signatures: ['sig1'] }, }; const encoded = SigAll.serializePackage(pkg); @@ -192,7 +185,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [{ secret: 'testsecret', C: '02' + '1'.repeat(64) }], outputs: [largeBm], - digests: SigAll.computeDigests([dummyProof], [largeBm]), }; const parsed = SigAll.deserializePackage(SigAll.serializePackage(pkg)); expect(parsed.outputs[0].amount.equals(largeAmount)).toBeTruthy(); @@ -205,10 +197,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [{ secret: 'testsecret', C: '02' + '1'.repeat(64) }], outputs: [{ amount: 32, id: 'bm1', B_: 'dummyB' }], - digests: SigAll.computeDigests( - [dummyProof], - [{ amount: Amount.from(32), id: 'bm1', B_: 'dummyB' }], - ), }), ); @@ -223,7 +211,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [{ secret: 'testsecret', C: '02' + '1'.repeat(64) }], outputs: [largeBm], - digests: SigAll.computeDigests([dummyProof], [largeBm]), }; const json = decodeRawJson(SigAll.serializePackage(pkg)); @@ -266,7 +253,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [], outputs: [], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('Invalid signing package version'); @@ -280,7 +266,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'unknown', inputs: [], outputs: [], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('Invalid signing package type'); @@ -294,7 +279,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: 'notanarray', outputs: [], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('inputs must be an array'); @@ -308,7 +292,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [{ C: 'abc' }], outputs: [], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('secret must be string'); @@ -322,7 +305,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [{ secret: 'x' }], outputs: [], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('C must be string'); @@ -336,7 +318,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [], outputs: 'notanarray', - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('outputs must be an array'); @@ -350,7 +331,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [], outputs: [null], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('Invalid output at index 0'); @@ -364,7 +344,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [], outputs: [{ B_: 'x', id: 'id1' }], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('amount must be a number or bigint'); @@ -378,7 +357,6 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [], outputs: [{ amount: 1, id: 'id1' }], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('B_ invalid'); @@ -392,55 +370,10 @@ describe('SigAll — serializePackage / deserializePackage', () => { type: 'swap', inputs: [], outputs: [{ amount: 1, B_: 'x' }], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('id invalid'); }); - - test('throws if digests.current is missing', () => { - expect(() => - SigAll.deserializePackage( - encodeRaw({ - version: 'sigallA', - type: 'swap', - inputs: [], - outputs: [], - }), - ), - ).toThrow('digests.current is required'); - }); - - test('throws if digests.current is empty string', () => { - expect(() => - SigAll.deserializePackage( - encodeRaw({ - version: 'sigallA', - type: 'swap', - inputs: [], - outputs: [], - digests: { current: '' }, - }), - ), - ).toThrow('digests.current is required'); - }); - - test('digest validation passes on correct digest', () => { - const pkg = SigAll.extractSwapPackage(makeSwapPreview()); - const encoded = SigAll.serializePackage(pkg); - expect(() => SigAll.deserializePackage(encoded, { validateDigest: true })).not.toThrow(); - }); - - test('digest validation throws on tampered current digest', () => { - const pkg = SigAll.extractSwapPackage(makeSwapPreview()); - const tampered = { - ...pkg, - digests: { ...pkg.digests, current: pkg.digests.current.slice(0, 63) + '0' }, - }; - expect(() => - SigAll.deserializePackage(SigAll.serializePackage(tampered), { validateDigest: true }), - ).toThrow('Digest validation failed'); - }); }); describe('SigAll — signPackage', () => { @@ -450,24 +383,6 @@ describe('SigAll — signPackage', () => { expect(signed.witness?.signatures.length).toBeGreaterThan(0); }); - test('signs both legacy and current when legacy is present', () => { - const pkg = SigAll.extractSwapPackage(makeSwapPreview()); - expect(pkg.digests.legacy).toBeDefined(); - const signed = SigAll.signPackage(pkg, dummyPrivkey); - // legacy + current = 2 signatures - expect(signed.witness?.signatures.length).toBe(2); - }); - - test('signs only current when legacy is absent', () => { - const pkg = SigAll.extractSwapPackage(makeSwapPreview()); - const noLegacy: SigAllSigningPackage = { - ...pkg, - digests: { current: pkg.digests.current }, - }; - const signed = SigAll.signPackage(noLegacy, dummyPrivkey); - expect(signed.witness?.signatures.length).toBe(1); - }); - test('accumulates signatures across multiple signers (multi-party)', () => { const pkg = SigAll.extractSwapPackage(makeSwapPreview()); const s1 = SigAll.signPackage(pkg, dummyPrivkey); @@ -492,7 +407,7 @@ describe('SigAll — signPackage', () => { describe('SigAll — signDigest', () => { test('produces a hex string signature', () => { const digests = SigAll.computeDigests([dummyProof], [dummyBlindedMessage]); - const sig = SigAll.signDigest(digests.current, dummyPrivkey); + const sig = SigAll.signDigest(digests.v0, dummyPrivkey); expect(typeof sig).toBe('string'); expect(sig.length).toBeGreaterThan(0); }); @@ -595,7 +510,7 @@ describe('SigAll — full transport roundtrip', () => { }); describe('SigAll — serializePackage omits falsy optional fields', () => { - test('empty/falsy quote, digests and witness are not emitted', () => { + test('empty/falsy quote and witness are not emitted', () => { // serializePackage only adds a key when its value is truthy; falsy optionals // (eg an empty quote) must stay out of the transport JSON. const pkg = { @@ -604,13 +519,11 @@ describe('SigAll — serializePackage omits falsy optional fields', () => { quote: '', inputs: [{ secret: 'testsecret', C: '02' + '1'.repeat(64) }], outputs: [dummyBlindedMessage], - digests: null, witness: null, } as unknown as SigAllSigningPackage; const json = decodeRawJson(SigAll.serializePackage(pkg)); expect(json).not.toContain('"quote"'); - expect(json).not.toContain('"digests"'); expect(json).not.toContain('"witness"'); }); }); @@ -638,7 +551,6 @@ describe('SigAll — deserializePackage input/output shape guards', () => { type: 'swap', inputs: [null], outputs: [], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('Invalid input at index 0'); @@ -652,7 +564,6 @@ describe('SigAll — deserializePackage input/output shape guards', () => { type: 'swap', inputs: [5], outputs: [], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('Invalid input at index 0'); @@ -666,7 +577,6 @@ describe('SigAll — deserializePackage input/output shape guards', () => { type: 'swap', inputs: [], outputs: [5], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('Invalid output at index 0'); @@ -680,7 +590,6 @@ describe('SigAll — deserializePackage input/output shape guards', () => { type: 'swap', inputs: [], outputs: [{ amount: 1, B_: 123, id: 'id1' }], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('B_ invalid'); @@ -694,13 +603,12 @@ describe('SigAll — deserializePackage input/output shape guards', () => { type: 'swap', inputs: [], outputs: [{ amount: 1, B_: 'x', id: 123 }], - digests: { current: 'a'.repeat(64) }, }), ), ).toThrow('id invalid'); }); - test('rejects a non-string digests.current', () => { + test('rejects a non-string-array witness.signatures', () => { expect(() => SigAll.deserializePackage( encodeRaw({ @@ -708,34 +616,10 @@ describe('SigAll — deserializePackage input/output shape guards', () => { type: 'swap', inputs: [], outputs: [], - digests: { current: 123 }, + witness: { signatures: [123] }, }), ), - ).toThrow('digests.current is required'); - }); -}); - -describe('SigAll — deserializePackage legacy digest validation', () => { - test('throws when only the legacy digest is tampered', () => { - // current digest stays valid so validation must fall through to the legacy check. - const pkg = SigAll.extractSwapPackage(makeSwapPreview()); - const tampered = { - ...pkg, - digests: { current: pkg.digests.current, legacy: pkg.digests.legacy!.slice(0, 63) + '0' }, - }; - expect(() => - SigAll.deserializePackage(SigAll.serializePackage(tampered), { validateDigest: true }), - ).toThrow('legacy digest mismatch'); - }); -}); - -describe('SigAll — signPackage requires a current digest', () => { - test('throws when digests is absent', () => { - const pkg = SigAll.extractSwapPackage(makeSwapPreview()); - const noDigests = { ...pkg, digests: undefined } as unknown as SigAllSigningPackage; - expect(() => SigAll.signPackage(noDigests, dummyPrivkey)).toThrow( - 'digests.current is required to sign package', - ); + ).toThrow('witness.signatures must be a string array'); }); }); @@ -785,3 +669,103 @@ describe('SigAll — mergeSignatures edge cases', () => { expect((merged.inputs[0].witness as { preimage?: string }).preimage).toBe('deadbeef'); }); }); + +describe('SigAll — signing binds to package contents', () => { + const signerPubkey = () => Bytes.toHex(getPubKeyFromPrivKey(Bytes.fromHex(dummyPrivkey))); + + test('signPackage emits one signature, verifiable over the recomputed v0 digest', () => { + const pkg = SigAll.extractSwapPackage(makeSwapPreview()); + const signed = SigAll.signPackage(pkg, dummyPrivkey); + + expect(signed.witness?.signatures).toHaveLength(1); + const digest = SigAll.computeDigests(pkg.inputs, pkg.outputs).v0; + expect(schnorrVerifyDigest(signed.witness!.signatures[0], digest, signerPubkey())).toBe(true); + }); + + test('signPackage does not sign the amount-blind concat of secrets and B_ values', () => { + const pkg = SigAll.extractSwapPackage(makeSwapPreview()); + const signed = SigAll.signPackage(pkg, dummyPrivkey); + + const concat = pkg.inputs.map((i) => i.secret).join('') + pkg.outputs.map((o) => o.B_).join(''); + const concatDigest = computeMessageDigest(concat, true); + for (const sig of signed.witness!.signatures) { + expect(schnorrVerifyDigest(sig, concatDigest, signerPubkey())).toBe(false); + } + }); + + test('a digests field smuggled into the package is ignored by sign and serialize', () => { + const pkg = SigAll.extractSwapPackage(makeSwapPreview()); + const dirty = { + ...pkg, + digests: { current: '00'.repeat(32), legacy: '11'.repeat(32) }, + } as unknown as SigAllSigningPackage; + + const signed = SigAll.signPackage(dirty, dummyPrivkey); + const digest = SigAll.computeDigests(pkg.inputs, pkg.outputs).v0; + expect(signed.witness?.signatures).toHaveLength(1); + expect(schnorrVerifyDigest(signed.witness!.signatures[0], digest, signerPubkey())).toBe(true); + + const rounded = SigAll.deserializePackage(SigAll.serializePackage(dirty)); + expect('digests' in rounded).toBe(false); + }); + + test('signPackage binds the melt quote', () => { + const pkg = SigAll.extractMeltPackage(makeMeltPreview()); + const signed = SigAll.signPackage(pkg, dummyPrivkey); + + const withQuote = SigAll.computeDigests(pkg.inputs, pkg.outputs, pkg.quote).v0; + const withoutQuote = SigAll.computeDigests(pkg.inputs, pkg.outputs).v0; + expect(schnorrVerifyDigest(signed.witness!.signatures[0], withQuote, signerPubkey())).toBe( + true, + ); + expect(schnorrVerifyDigest(signed.witness!.signatures[0], withoutQuote, signerPubkey())).toBe( + false, + ); + }); +}); + +describe('SigAll — transport format', () => { + test('serializePackage emits the sigallA prefix and no digests', () => { + const encoded = SigAll.serializePackage(SigAll.extractSwapPackage(makeSwapPreview())); + expect(encoded.startsWith('sigallA')).toBe(true); + expect(JSON.stringify(SigAll.deserializePackage(encoded))).not.toContain('digests'); + }); + + test('accepts a digest-carrying package from an older build and strips the field', () => { + const parsed = SigAll.deserializePackage( + encodeRaw({ + version: 'sigallA', + type: 'swap', + inputs: [{ secret: 'testsecret', C: '02' + '1'.repeat(64) }], + outputs: [{ amount: 32, id: 'bm1', B_: 'dummyB' }], + digests: { current: 'a'.repeat(64), legacy: 'b'.repeat(64) }, + }), + ); + + expect('digests' in parsed).toBe(false); + expect(parsed.inputs).toHaveLength(1); + }); + + test('rejects a melt package without a quote', () => { + const pkg = SigAll.extractMeltPackage(makeMeltPreview()); + const encoded = SigAll.serializePackage({ ...pkg, quote: undefined }); + expect(() => SigAll.deserializePackage(encoded)).toThrow(/quote/); + }); +}); + +describe('SigAll — NUT-11 signing package vector', () => { + // Pinned byte-for-byte to the SigAllSigningPackage example in the NUT-11 test vectors. + const VECTOR = + 'sigallAeyJ2ZXJzaW9uIjoic2lnYWxsQSIsInR5cGUiOiJzd2FwIiwiaW5wdXRzIjpbeyJzZWNyZXQiOiJbXCJQMlBLXCIse1wibm9uY2VcIjpcImM3ZjI4MGViNTVjMWU4NTY0ZTAzZGIwNjk3M2U5NGJjOWI2NjZkOWUxY2E0MmFkMjc4NDA4ZmU2MjU5NTAzMDNcIixcImRhdGFcIjpcIjAzMGQ4YWNlZGZlMDcyYzlmYTQ0OWExZWZlMDgxNzE1NzQwM2ZiZWM0NjBkOGU3OWY5NTc5NjYwNTZlNWRkNzZjMVwiLFwidGFnc1wiOltbXCJzaWdmbGFnXCIsXCJTSUdfQUxMXCJdXX1dIiwiQyI6IjAyYzk3ZWUzZDFkYjQxY2YwYTNkZGI2MDE3MjRiZTg3MTFhMDMyOTUwODExYmYzMjZmODIxOWM1MGM0ODA4ZDNjZCJ9XSwib3V0cHV0cyI6W3siYW1vdW50IjoyLCJpZCI6IjAwYmZhNzMzMDJkMTJmZmQiLCJCXyI6IjAzOGVjODUzZDY1YWUxYjc5YjVjZGJjMjc3NDE1MGIyY2IyODhkNmQyNmUxMjk1OGExNmZiMzNjMzJkOWE4NmMzOSJ9XSwid2l0bmVzcyI6eyJzaWduYXR1cmVzIjpbImNlMDE3Y2EyNWIxYjk3ZGYyZjcyZTRiNDlmNjlhYzI2YTI0MGNlMTRiMzY5MGE4ZmU2MTlkNDFjY2M0MmQzYzEyODJlMDczZjg1YWNkMzZkYzUwMDExNjM4OTA2ZjM1YjU2NjE1ZjI0ZTRkMDNlOGVmZmU4MjU3ZjZhODA4NTM4Il19fQ'; + + test('round-trips byte-for-byte and carries a valid SIG_ALL signature', () => { + const pkg = SigAll.deserializePackage(VECTOR); + expect(SigAll.serializePackage(pkg)).toBe(VECTOR); + + // The witness signature verifies over the v0 digest recomputed from the + // package, for the lock pubkey inside the input secret. + const digest = SigAll.computeDigests(pkg.inputs, pkg.outputs).v0; + const lockPubkey = (JSON.parse(pkg.inputs[0].secret) as [string, { data: string }])[1].data; + expect(schnorrVerifyDigest(pkg.witness!.signatures[0], digest, lockPubkey)).toBe(true); + }); +}); From 184de821465e9fa0e00b3218a0357abc59bcf75b Mon Sep 17 00:00:00 2001 From: Rob Woodgate Date: Tue, 11 Aug 2026 22:00:46 +0100 Subject: [PATCH 2/2] chore(docker): update mint image pins to match main Nutshell 0.20.3 and mintd 0.17.3 / 0.17.3-rc.0. Nutshell releases before 0.20.3 do not verify the SIG_ALL message format the wallet now produces. --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 5c43d7065..986c20e21 100644 --- a/Makefile +++ b/Makefile @@ -13,12 +13,12 @@ RATE_LIMIT_PM ?= 200 # ------------------------ # Pin versions # ------------------------ -CDK_IMAGE_RC ?= cashubtc/mintd:0.14.3 -CDK_IMAGE ?= cashubtc/mintd:v0.16.0 +CDK_IMAGE_RC ?= cashubtc/mintd:0.17.3-rc.0 +CDK_IMAGE ?= cashubtc/mintd:0.17.3 CDK_NAME ?= cashu-dev-cdk NUT_IMAGE_RC ?= cashubtc/nutshell:0.18.2 -NUT_IMAGE ?= cashubtc/nutshell:0.20.0 +NUT_IMAGE ?= cashubtc/nutshell:0.20.3 NUT_NAME ?= cashu-dev-nutshell # ------------------------