diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts index 717a8b3386..a59ba857b1 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts @@ -56,6 +56,14 @@ export class DSG { return this.dsgState; } + getPartyIdx(): number { + return this.partyIdx; + } + + getOtherPartyIdx(): number | null { + return this.otherPartyIdx; + } + /** * Initialises the DSG session. The keyshare must come from a prior DKG run, and * `otherPartyIdx` must be the single counterpart who will co-sign with this party. diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts index f97d06a152..d19602424e 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts @@ -2,6 +2,8 @@ import crypto from 'crypto'; import assert from 'assert'; import { x25519 } from '@noble/curves/ed25519'; import { DKG } from './dkg'; +import { DSG } from './dsg'; +import { DeserializedMessages } from './types'; /** * Concatenates multiple Uint8Array instances into a single Uint8Array @@ -74,3 +76,47 @@ export async function generateEdDsaDKGKeyShares( return [user, backup, bitgo]; } + +/** + * Initializes two DSG parties and drives them through the protocol until the specified round. + * + * @param round - Round to execute until (1–3). Returns intermediate message arrays for 1–2, + * or the 64-byte Ed25519 signature Buffer for 3. + * @param party1Dsg - First DSG party (`new DSG(partyIdx)`), not yet initialized. + * @param party2Dsg - Second DSG party (`new DSG(partyIdx)`), not yet initialized. + * @param keyShare1 - Key share for the first party. + * @param keyShare2 - Key share for the second party. + * @param message - Raw message bytes to sign. + * @param derivationPath - BIP-32-style derivation path, e.g. `"m"` or `"m/0/0"`. + */ +export function executeTillRound( + round: number, + party1Dsg: DSG, + party2Dsg: DSG, + keyShare1: Buffer, + keyShare2: Buffer, + message: Buffer, + derivationPath: string +): DeserializedMessages[] | Buffer { + if (round < 1 || round > 3) { + throw Error('Invalid round number'); + } + party1Dsg.initDsg(keyShare1, message, derivationPath, party2Dsg.getPartyIdx()); + party2Dsg.initDsg(keyShare2, message, derivationPath, party1Dsg.getPartyIdx()); + const party1Round0Message = party1Dsg.getFirstMessage(); + const party2Round0Message = party2Dsg.getFirstMessage(); + + const [party2Round1Messages] = party2Dsg.handleIncomingMessages([party1Round0Message, party2Round0Message]); + const [party1Round1Messages] = party1Dsg.handleIncomingMessages([party1Round0Message, party2Round0Message]); + if (round === 1) return [[party1Round1Messages], [party2Round1Messages]]; + + const [party1Round2Messages] = party1Dsg.handleIncomingMessages([party1Round1Messages, party2Round1Messages]); + const [party2Round2Messages] = party2Dsg.handleIncomingMessages([party1Round1Messages, party2Round1Messages]); + if (round === 2) return [[party1Round2Messages], [party2Round2Messages]]; + + party1Dsg.handleIncomingMessages([party1Round2Messages, party2Round2Messages]); + party2Dsg.handleIncomingMessages([party1Round2Messages, party2Round2Messages]); + + assert(party1Dsg.getSignature().toString('hex') === party2Dsg.getSignature().toString('hex')); + return party1Dsg.getSignature(); +} diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts index 49fbe64e6f..edf89c6a95 100644 --- a/modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts @@ -1,7 +1,7 @@ import assert from 'assert'; import { ed25519 } from '@noble/curves/ed25519'; -import { EddsaMPSDkg, EddsaMPSDsg, MPSTypes } from '../../../../src/tss/eddsa-mps'; -import { generateEdDsaDKGKeyShares, runEdDsaDSG } from './util'; +import { EddsaMPSDkg, EddsaMPSDsg, MPSTypes, MPSUtil } from '../../../../src/tss/eddsa-mps'; +import { generateEdDsaDKGKeyShares } from './util'; const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks'); @@ -84,7 +84,9 @@ describe('EdDSA MPS DSG', function () { describe('DSG Protocol Execution (2-of-3)', function () { it('should complete full DSG between user (0) and bitgo (2) and produce identical signatures', function () { - const { dsgA, dsgB } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE); + const dsgA = new EddsaMPSDsg.DSG(0); + const dsgB = new EddsaMPSDsg.DSG(2); + MPSUtil.executeTillRound(3, dsgA, dsgB, userKeyShare, bitgoKeyShare, MESSAGE, 'm'); assert.strictEqual(dsgA.getState(), 'Complete'); assert.strictEqual(dsgB.getState(), 'Complete'); @@ -97,17 +99,48 @@ describe('EdDSA MPS DSG', function () { }); it('should produce a signature that verifies under the DKG public key', function () { - const { dsgA } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE); - const sig = dsgA.getSignature(); + const sig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + userKeyShare, + bitgoKeyShare, + MESSAGE, + 'm' + ) as Buffer; const isValid = ed25519.verify(sig, MESSAGE, dkgPubKey); assert(isValid, 'Signature should verify under DKG public key'); }); it('should sign the same message identically across all 2-of-3 party combinations', function () { - const userBackupSig = runEdDsaDSG(userKeyShare, backupKeyShare, 0, 1, MESSAGE).dsgA.getSignature(); - const backupBitgoSig = runEdDsaDSG(backupKeyShare, bitgoKeyShare, 1, 2, MESSAGE).dsgA.getSignature(); - const userBitgoSig = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE).dsgA.getSignature(); + const userBackupSig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(1), + userKeyShare, + backupKeyShare, + MESSAGE, + 'm' + ) as Buffer; + const backupBitgoSig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(1), + new EddsaMPSDsg.DSG(2), + backupKeyShare, + bitgoKeyShare, + MESSAGE, + 'm' + ) as Buffer; + const userBitgoSig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + userKeyShare, + bitgoKeyShare, + MESSAGE, + 'm' + ) as Buffer; // Per-session nonce randomisation means signatures across DIFFERENT signing // sessions WILL differ. The invariant we test is that every 2-of-3 subset @@ -121,27 +154,78 @@ describe('EdDSA MPS DSG', function () { const shortMsg = Buffer.from([0x01]); const longMsg = Buffer.alloc(4096, 0xab); - const { dsgA: short } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, shortMsg); - const { dsgA: long } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, longMsg); - - assert(ed25519.verify(short.getSignature(), shortMsg, dkgPubKey), '1-byte message signature should verify'); - assert(ed25519.verify(long.getSignature(), longMsg, dkgPubKey), '4096-byte message signature should verify'); + const shortSig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + userKeyShare, + bitgoKeyShare, + shortMsg, + 'm' + ) as Buffer; + const longSig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + userKeyShare, + bitgoKeyShare, + longMsg, + 'm' + ) as Buffer; + + assert(ed25519.verify(shortSig, shortMsg, dkgPubKey), '1-byte message signature should verify'); + assert(ed25519.verify(longSig, longMsg, dkgPubKey), '4096-byte message signature should verify'); }); it('should throw when handleIncomingMessages is called after completion', function () { - const { dsgA } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE); + const dsgA = new EddsaMPSDsg.DSG(0); + MPSUtil.executeTillRound(3, dsgA, new EddsaMPSDsg.DSG(2), userKeyShare, bitgoKeyShare, MESSAGE, 'm'); assert.throws(() => dsgA.handleIncomingMessages([]), /already completed/); }); + + it('should fail when parties sign different messages', function () { + const dsg1 = new EddsaMPSDsg.DSG(0); + const dsg2 = new EddsaMPSDsg.DSG(2); + dsg1.initDsg(userKeyShare, Buffer.from('MESSAGE'), 'm', 2); + dsg2.initDsg(bitgoKeyShare, Buffer.from('DIFFERENT_MESSAGE'), 'm', 0); + + const r0_1 = dsg1.getFirstMessage(); + const r0_2 = dsg2.getFirstMessage(); + + const [r1_1] = dsg1.handleIncomingMessages([r0_1, r0_2]); + const [r1_2] = dsg2.handleIncomingMessages([r0_1, r0_2]); + + assert.throws( + () => dsg1.handleIncomingMessages([r1_1, r1_2]), + /Error while creating messages from party 0, round WaitMsg2: Protocol Error/ + ); + }); }); describe('Derivation Paths', function () { it('should produce different signatures for different derivation paths', function () { - const { dsgA: rootSig } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE, 'm'); - const { dsgA: derivedSig } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE, 'm/0/1'); + const rootSig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + userKeyShare, + bitgoKeyShare, + MESSAGE, + 'm' + ) as Buffer; + const derivedSig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + userKeyShare, + bitgoKeyShare, + MESSAGE, + 'm/0/1' + ) as Buffer; assert.notStrictEqual( - rootSig.getSignature().toString('hex'), - derivedSig.getSignature().toString('hex'), + rootSig.toString('hex'), + derivedSig.toString('hex'), 'Different derivation paths should produce different signatures' ); }); @@ -238,7 +322,9 @@ describe('EdDSA MPS DSG', function () { }); it('should throw when exporting session after completion', function () { - const { dsgA, dsgB } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE); + const dsgA = new EddsaMPSDsg.DSG(0); + const dsgB = new EddsaMPSDsg.DSG(2); + MPSUtil.executeTillRound(3, dsgA, dsgB, userKeyShare, bitgoKeyShare, MESSAGE, 'm'); assert.throws(() => dsgA.getSession(), /DSG session is complete\. Exporting the session is not allowed\./); assert.throws(() => dsgB.getSession(), /DSG session is complete\. Exporting the session is not allowed\./); }); diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts index 32689be628..1102468c8f 100644 --- a/modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts @@ -1,4 +1,6 @@ import assert from 'assert'; +import { ed25519 } from '@noble/curves/ed25519'; +import { EddsaMPSDkg, EddsaMPSDsg, MPSUtil } from '../../../../src/tss/eddsa-mps'; import { concatBytes, generateEdDsaDKGKeyShares } from '../../../../src/tss/eddsa-mps/util'; describe('EdDSA Utility Functions', function () { @@ -51,4 +53,115 @@ describe('EdDSA Utility Functions', function () { ); }); }); + + describe('executeTillRound', function () { + const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks'); + + let userDkg: EddsaMPSDkg.DKG; + let keySharesByIdx: [Buffer, Buffer, Buffer]; + let dkgPubKey: Buffer; + + before(async function () { + const [user, backup, bitgo] = await generateEdDsaDKGKeyShares(); + userDkg = user; + keySharesByIdx = [user.getKeyShare(), backup.getKeyShare(), bitgo.getKeyShare()]; + dkgPubKey = user.getSharePublicKey(); + }); + + // All three 2-of-3 signing combinations: user+backup, user+BitGo, backup+BitGo. + const PARTY_PAIRS: Array<[number, number]> = [ + [0, 1], + [0, 2], + [1, 2], + ]; + + PARTY_PAIRS.forEach(([p1, p2]) => { + it(`should produce a valid signature verifying under the DKG public key for parties ${p1}+${p2}`, function () { + const sig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(p1), + new EddsaMPSDsg.DSG(p2), + keySharesByIdx[p1], + keySharesByIdx[p2], + MESSAGE, + 'm' + ) as Buffer; + assert.strictEqual(sig.length, 64); + assert(ed25519.verify(sig, MESSAGE, dkgPubKey)); + }); + }); + + it('should verify round-3 signature against root public key from getCommonKeychain()', function () { + const sig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + keySharesByIdx[0], + keySharesByIdx[2], + MESSAGE, + 'm' + ) as Buffer; + const rootPubKey = Buffer.from(userDkg.getCommonKeychain().slice(0, 64), 'hex'); + assert(ed25519.verify(sig, MESSAGE, rootPubKey), 'should verify under root public key from getCommonKeychain()'); + }); + + it('should not verify under the root public key when signing at a derived path (m/0/0)', function () { + const sig = MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + keySharesByIdx[0], + keySharesByIdx[2], + MESSAGE, + 'm/0/0' + ) as Buffer; + assert.strictEqual(sig.length, 64, 'Derived path signature must be 64 bytes'); + const rootPubKey = Buffer.from(userDkg.getCommonKeychain().slice(0, 64), 'hex'); + assert( + !ed25519.verify(sig, MESSAGE, rootPubKey), + 'derived-path signature should not verify under root public key' + ); + }); + + it('should return message arrays (not a Buffer) for intermediate round 1', function () { + const result = MPSUtil.executeTillRound( + 1, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + keySharesByIdx[0], + keySharesByIdx[2], + MESSAGE, + 'm' + ); + assert(!Buffer.isBuffer(result), 'round 1 should return message arrays, not a Buffer'); + assert.strictEqual(result.length, 2, 'should contain message arrays for both parties'); + }); + + it('should return message arrays (not a Buffer) for intermediate round 2', function () { + const result = MPSUtil.executeTillRound( + 2, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + keySharesByIdx[0], + keySharesByIdx[2], + MESSAGE, + 'm' + ); + assert(!Buffer.isBuffer(result), 'round 2 should return message arrays, not a Buffer'); + assert.strictEqual(result.length, 2, 'should contain message arrays for both parties'); + }); + + it('should throw for round out of range', function () { + const dsg1 = new EddsaMPSDsg.DSG(0); + const dsg2 = new EddsaMPSDsg.DSG(2); + assert.throws( + () => MPSUtil.executeTillRound(0, dsg1, dsg2, keySharesByIdx[0], keySharesByIdx[2], MESSAGE, 'm'), + /Invalid round number/ + ); + assert.throws( + () => MPSUtil.executeTillRound(4, dsg1, dsg2, keySharesByIdx[0], keySharesByIdx[2], MESSAGE, 'm'), + /Invalid round number/ + ); + }); + }); }); diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts index 0005208e85..211ab0dfce 100644 --- a/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts @@ -1,47 +1,3 @@ -import { EddsaMPSDsg } from '../../../../src/tss/eddsa-mps'; -import { DeserializedMessage } from '../../../../src/tss/eddsa-mps/types'; - // Re-export the production helper so existing tests can resolve via './util' // without a separate, drifting copy. export { generateEdDsaDKGKeyShares } from '../../../../src/tss/eddsa-mps/util'; - -/** - * Runs a full 2-of-3 EdDSA DSG protocol between two parties holding `keyShareA` - * and `keyShareB`, signing `message` under `derivationPath`. - * - * Returns both parties' resulting `DSG` instances so callers can compare signatures - * (`dsgA.getSignature()` and `dsgB.getSignature()` should be byte-identical) or - * verify against a public key. - */ -export function runEdDsaDSG( - keyShareA: Buffer, - keyShareB: Buffer, - partyAIdx: number, - partyBIdx: number, - message: Buffer, - derivationPath = 'm' -): { dsgA: EddsaMPSDsg.DSG; dsgB: EddsaMPSDsg.DSG } { - const dsgA = new EddsaMPSDsg.DSG(partyAIdx); - const dsgB = new EddsaMPSDsg.DSG(partyBIdx); - - dsgA.initDsg(keyShareA, message, derivationPath, partyBIdx); - dsgB.initDsg(keyShareB, message, derivationPath, partyAIdx); - - // Round 0 -> SignMsg1 - const a0: DeserializedMessage = dsgA.getFirstMessage(); - const b0: DeserializedMessage = dsgB.getFirstMessage(); - - // Round 1 -> SignMsg2 - const [a1] = dsgA.handleIncomingMessages([a0, b0]); - const [b1] = dsgB.handleIncomingMessages([a0, b0]); - - // Round 2 -> SignMsg3 (partial sig) - const [a2] = dsgA.handleIncomingMessages([a1, b1]); - const [b2] = dsgB.handleIncomingMessages([a1, b1]); - - // Round 3 -> Complete (no output messages) - dsgA.handleIncomingMessages([a2, b2]); - dsgB.handleIncomingMessages([a2, b2]); - - return { dsgA, dsgB }; -}