Skip to content

Commit 02a232a

Browse files
committed
feat(sdk-lib-mpc): add executeTillRound handler util
Adds an executeTillRound orchestration utility for the EdDSA MPSv2 DSG protocol, mirroring DklsUtils.executeTillRound for ECDSA DKLS. Also exposes getPartyIdx and getOtherPartyIdx accessors on the DSG class to support index-free wiring inside the utility. - Add executeTillRound to modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts; accepts two un-initialized DSG instances plus key shares, message, and derivation path, calls initDsg internally using getPartyIdx(), and drives all 3 interactive rounds (WaitMsg1 -> WaitMsg2 -> WaitMsg3 -> Complete) - Return intermediate DeserializedMessages[][] for rounds 1-2 and the final 64-byte Ed25519 signature Buffer for round 3 - Add getPartyIdx() and getOtherPartyIdx() accessors to DSG class - Add unit tests in modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts for executeTillRound: per-pair signatures across all three 2-of-3 party combinations (0+1, 0+2, 1+2), root-key verification via getCommonKeychain(), derived-path (m/0/0) key isolation check, intermediate round return-type assertions, and out-of-range round error handling - Migrate existing dsg.ts callers from the local runEdDsaDSG test helper to MPSUtil.executeTillRound and remove runEdDsaDSG from test/unit/tss/eddsa/util.ts - Add dsg.ts protocol test asserting wasm-mps rejects cross-message signing with the wrapped "round WaitMsg2: Protocol Error" failure Ticket: WCI-386
1 parent c1af298 commit 02a232a

5 files changed

Lines changed: 272 additions & 63 deletions

File tree

modules/sdk-lib-mpc/src/tss/eddsa-mps/dsg.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ export class DSG {
5656
return this.dsgState;
5757
}
5858

59+
getPartyIdx(): number {
60+
return this.partyIdx;
61+
}
62+
63+
getOtherPartyIdx(): number | null {
64+
return this.otherPartyIdx;
65+
}
66+
5967
/**
6068
* Initialises the DSG session. The keyshare must come from a prior DKG run, and
6169
* `otherPartyIdx` must be the single counterpart who will co-sign with this party.

modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import crypto from 'crypto';
22
import assert from 'assert';
33
import { x25519 } from '@noble/curves/ed25519';
44
import { DKG } from './dkg';
5+
import { DSG } from './dsg';
6+
import { DeserializedMessages } from './types';
57

68
/**
79
* Concatenates multiple Uint8Array instances into a single Uint8Array
@@ -74,3 +76,47 @@ export async function generateEdDsaDKGKeyShares(
7476

7577
return [user, backup, bitgo];
7678
}
79+
80+
/**
81+
* Initializes two DSG parties and drives them through the protocol until the specified round.
82+
*
83+
* @param round - Round to execute until (1–3). Returns intermediate message arrays for 1–2,
84+
* or the 64-byte Ed25519 signature Buffer for 3.
85+
* @param party1Dsg - First DSG party (`new DSG(partyIdx)`), not yet initialized.
86+
* @param party2Dsg - Second DSG party (`new DSG(partyIdx)`), not yet initialized.
87+
* @param keyShare1 - Key share for the first party.
88+
* @param keyShare2 - Key share for the second party.
89+
* @param message - Raw message bytes to sign.
90+
* @param derivationPath - BIP-32-style derivation path, e.g. `"m"` or `"m/0/0"`.
91+
*/
92+
export function executeTillRound(
93+
round: number,
94+
party1Dsg: DSG,
95+
party2Dsg: DSG,
96+
keyShare1: Buffer,
97+
keyShare2: Buffer,
98+
message: Buffer,
99+
derivationPath: string
100+
): DeserializedMessages[] | Buffer {
101+
if (round < 1 || round > 3) {
102+
throw Error('Invalid round number');
103+
}
104+
party1Dsg.initDsg(keyShare1, message, derivationPath, party2Dsg.getPartyIdx());
105+
party2Dsg.initDsg(keyShare2, message, derivationPath, party1Dsg.getPartyIdx());
106+
const party1Round0Message = party1Dsg.getFirstMessage();
107+
const party2Round0Message = party2Dsg.getFirstMessage();
108+
109+
const [party2Round1Messages] = party2Dsg.handleIncomingMessages([party1Round0Message, party2Round0Message]);
110+
const [party1Round1Messages] = party1Dsg.handleIncomingMessages([party1Round0Message, party2Round0Message]);
111+
if (round === 1) return [[party1Round1Messages], [party2Round1Messages]];
112+
113+
const [party1Round2Messages] = party1Dsg.handleIncomingMessages([party1Round1Messages, party2Round1Messages]);
114+
const [party2Round2Messages] = party2Dsg.handleIncomingMessages([party1Round1Messages, party2Round1Messages]);
115+
if (round === 2) return [[party1Round2Messages], [party2Round2Messages]];
116+
117+
party1Dsg.handleIncomingMessages([party1Round2Messages, party2Round2Messages]);
118+
party2Dsg.handleIncomingMessages([party1Round2Messages, party2Round2Messages]);
119+
120+
assert(party1Dsg.getSignature().toString('hex') === party2Dsg.getSignature().toString('hex'));
121+
return party1Dsg.getSignature();
122+
}

modules/sdk-lib-mpc/test/unit/tss/eddsa/dsg.ts

Lines changed: 105 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'assert';
22
import { ed25519 } from '@noble/curves/ed25519';
3-
import { EddsaMPSDkg, EddsaMPSDsg, MPSTypes } from '../../../../src/tss/eddsa-mps';
4-
import { generateEdDsaDKGKeyShares, runEdDsaDSG } from './util';
3+
import { EddsaMPSDkg, EddsaMPSDsg, MPSTypes, MPSUtil } from '../../../../src/tss/eddsa-mps';
4+
import { generateEdDsaDKGKeyShares } from './util';
55

66
const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks');
77

@@ -84,7 +84,9 @@ describe('EdDSA MPS DSG', function () {
8484

8585
describe('DSG Protocol Execution (2-of-3)', function () {
8686
it('should complete full DSG between user (0) and bitgo (2) and produce identical signatures', function () {
87-
const { dsgA, dsgB } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE);
87+
const dsgA = new EddsaMPSDsg.DSG(0);
88+
const dsgB = new EddsaMPSDsg.DSG(2);
89+
MPSUtil.executeTillRound(3, dsgA, dsgB, userKeyShare, bitgoKeyShare, MESSAGE, 'm');
8890

8991
assert.strictEqual(dsgA.getState(), 'Complete');
9092
assert.strictEqual(dsgB.getState(), 'Complete');
@@ -97,17 +99,48 @@ describe('EdDSA MPS DSG', function () {
9799
});
98100

99101
it('should produce a signature that verifies under the DKG public key', function () {
100-
const { dsgA } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE);
101-
const sig = dsgA.getSignature();
102+
const sig = MPSUtil.executeTillRound(
103+
3,
104+
new EddsaMPSDsg.DSG(0),
105+
new EddsaMPSDsg.DSG(2),
106+
userKeyShare,
107+
bitgoKeyShare,
108+
MESSAGE,
109+
'm'
110+
) as Buffer;
102111

103112
const isValid = ed25519.verify(sig, MESSAGE, dkgPubKey);
104113
assert(isValid, 'Signature should verify under DKG public key');
105114
});
106115

107116
it('should sign the same message identically across all 2-of-3 party combinations', function () {
108-
const userBackupSig = runEdDsaDSG(userKeyShare, backupKeyShare, 0, 1, MESSAGE).dsgA.getSignature();
109-
const backupBitgoSig = runEdDsaDSG(backupKeyShare, bitgoKeyShare, 1, 2, MESSAGE).dsgA.getSignature();
110-
const userBitgoSig = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE).dsgA.getSignature();
117+
const userBackupSig = MPSUtil.executeTillRound(
118+
3,
119+
new EddsaMPSDsg.DSG(0),
120+
new EddsaMPSDsg.DSG(1),
121+
userKeyShare,
122+
backupKeyShare,
123+
MESSAGE,
124+
'm'
125+
) as Buffer;
126+
const backupBitgoSig = MPSUtil.executeTillRound(
127+
3,
128+
new EddsaMPSDsg.DSG(1),
129+
new EddsaMPSDsg.DSG(2),
130+
backupKeyShare,
131+
bitgoKeyShare,
132+
MESSAGE,
133+
'm'
134+
) as Buffer;
135+
const userBitgoSig = MPSUtil.executeTillRound(
136+
3,
137+
new EddsaMPSDsg.DSG(0),
138+
new EddsaMPSDsg.DSG(2),
139+
userKeyShare,
140+
bitgoKeyShare,
141+
MESSAGE,
142+
'm'
143+
) as Buffer;
111144

112145
// Per-session nonce randomisation means signatures across DIFFERENT signing
113146
// sessions WILL differ. The invariant we test is that every 2-of-3 subset
@@ -121,27 +154,78 @@ describe('EdDSA MPS DSG', function () {
121154
const shortMsg = Buffer.from([0x01]);
122155
const longMsg = Buffer.alloc(4096, 0xab);
123156

124-
const { dsgA: short } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, shortMsg);
125-
const { dsgA: long } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, longMsg);
126-
127-
assert(ed25519.verify(short.getSignature(), shortMsg, dkgPubKey), '1-byte message signature should verify');
128-
assert(ed25519.verify(long.getSignature(), longMsg, dkgPubKey), '4096-byte message signature should verify');
157+
const shortSig = MPSUtil.executeTillRound(
158+
3,
159+
new EddsaMPSDsg.DSG(0),
160+
new EddsaMPSDsg.DSG(2),
161+
userKeyShare,
162+
bitgoKeyShare,
163+
shortMsg,
164+
'm'
165+
) as Buffer;
166+
const longSig = MPSUtil.executeTillRound(
167+
3,
168+
new EddsaMPSDsg.DSG(0),
169+
new EddsaMPSDsg.DSG(2),
170+
userKeyShare,
171+
bitgoKeyShare,
172+
longMsg,
173+
'm'
174+
) as Buffer;
175+
176+
assert(ed25519.verify(shortSig, shortMsg, dkgPubKey), '1-byte message signature should verify');
177+
assert(ed25519.verify(longSig, longMsg, dkgPubKey), '4096-byte message signature should verify');
129178
});
130179

131180
it('should throw when handleIncomingMessages is called after completion', function () {
132-
const { dsgA } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE);
181+
const dsgA = new EddsaMPSDsg.DSG(0);
182+
MPSUtil.executeTillRound(3, dsgA, new EddsaMPSDsg.DSG(2), userKeyShare, bitgoKeyShare, MESSAGE, 'm');
133183
assert.throws(() => dsgA.handleIncomingMessages([]), /already completed/);
134184
});
185+
186+
it('should fail when parties sign different messages', function () {
187+
const dsg1 = new EddsaMPSDsg.DSG(0);
188+
const dsg2 = new EddsaMPSDsg.DSG(2);
189+
dsg1.initDsg(userKeyShare, Buffer.from('MESSAGE'), 'm', 2);
190+
dsg2.initDsg(bitgoKeyShare, Buffer.from('DIFFERENT_MESSAGE'), 'm', 0);
191+
192+
const r0_1 = dsg1.getFirstMessage();
193+
const r0_2 = dsg2.getFirstMessage();
194+
195+
const [r1_1] = dsg1.handleIncomingMessages([r0_1, r0_2]);
196+
const [r1_2] = dsg2.handleIncomingMessages([r0_1, r0_2]);
197+
198+
assert.throws(
199+
() => dsg1.handleIncomingMessages([r1_1, r1_2]),
200+
/Error while creating messages from party 0, round WaitMsg2: Protocol Error/
201+
);
202+
});
135203
});
136204

137205
describe('Derivation Paths', function () {
138206
it('should produce different signatures for different derivation paths', function () {
139-
const { dsgA: rootSig } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE, 'm');
140-
const { dsgA: derivedSig } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE, 'm/0/1');
207+
const rootSig = MPSUtil.executeTillRound(
208+
3,
209+
new EddsaMPSDsg.DSG(0),
210+
new EddsaMPSDsg.DSG(2),
211+
userKeyShare,
212+
bitgoKeyShare,
213+
MESSAGE,
214+
'm'
215+
) as Buffer;
216+
const derivedSig = MPSUtil.executeTillRound(
217+
3,
218+
new EddsaMPSDsg.DSG(0),
219+
new EddsaMPSDsg.DSG(2),
220+
userKeyShare,
221+
bitgoKeyShare,
222+
MESSAGE,
223+
'm/0/1'
224+
) as Buffer;
141225

142226
assert.notStrictEqual(
143-
rootSig.getSignature().toString('hex'),
144-
derivedSig.getSignature().toString('hex'),
227+
rootSig.toString('hex'),
228+
derivedSig.toString('hex'),
145229
'Different derivation paths should produce different signatures'
146230
);
147231
});
@@ -238,7 +322,9 @@ describe('EdDSA MPS DSG', function () {
238322
});
239323

240324
it('should throw when exporting session after completion', function () {
241-
const { dsgA, dsgB } = runEdDsaDSG(userKeyShare, bitgoKeyShare, 0, 2, MESSAGE);
325+
const dsgA = new EddsaMPSDsg.DSG(0);
326+
const dsgB = new EddsaMPSDsg.DSG(2);
327+
MPSUtil.executeTillRound(3, dsgA, dsgB, userKeyShare, bitgoKeyShare, MESSAGE, 'm');
242328
assert.throws(() => dsgA.getSession(), /DSG session is complete\. Exporting the session is not allowed\./);
243329
assert.throws(() => dsgB.getSession(), /DSG session is complete\. Exporting the session is not allowed\./);
244330
});

modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import assert from 'assert';
2+
import { ed25519 } from '@noble/curves/ed25519';
3+
import { EddsaMPSDkg, EddsaMPSDsg, MPSUtil } from '../../../../src/tss/eddsa-mps';
24
import { concatBytes, generateEdDsaDKGKeyShares } from '../../../../src/tss/eddsa-mps/util';
35

46
describe('EdDSA Utility Functions', function () {
@@ -51,4 +53,115 @@ describe('EdDSA Utility Functions', function () {
5153
);
5254
});
5355
});
56+
57+
describe('executeTillRound', function () {
58+
const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks');
59+
60+
let userDkg: EddsaMPSDkg.DKG;
61+
let keySharesByIdx: [Buffer, Buffer, Buffer];
62+
let dkgPubKey: Buffer;
63+
64+
before(async function () {
65+
const [user, backup, bitgo] = await generateEdDsaDKGKeyShares();
66+
userDkg = user;
67+
keySharesByIdx = [user.getKeyShare(), backup.getKeyShare(), bitgo.getKeyShare()];
68+
dkgPubKey = user.getSharePublicKey();
69+
});
70+
71+
// All three 2-of-3 signing combinations: user+backup, user+BitGo, backup+BitGo.
72+
const PARTY_PAIRS: Array<[number, number]> = [
73+
[0, 1],
74+
[0, 2],
75+
[1, 2],
76+
];
77+
78+
PARTY_PAIRS.forEach(([p1, p2]) => {
79+
it(`should produce a valid signature verifying under the DKG public key for parties ${p1}+${p2}`, function () {
80+
const sig = MPSUtil.executeTillRound(
81+
3,
82+
new EddsaMPSDsg.DSG(p1),
83+
new EddsaMPSDsg.DSG(p2),
84+
keySharesByIdx[p1],
85+
keySharesByIdx[p2],
86+
MESSAGE,
87+
'm'
88+
) as Buffer;
89+
assert.strictEqual(sig.length, 64);
90+
assert(ed25519.verify(sig, MESSAGE, dkgPubKey));
91+
});
92+
});
93+
94+
it('should verify round-3 signature against root public key from getCommonKeychain()', function () {
95+
const sig = MPSUtil.executeTillRound(
96+
3,
97+
new EddsaMPSDsg.DSG(0),
98+
new EddsaMPSDsg.DSG(2),
99+
keySharesByIdx[0],
100+
keySharesByIdx[2],
101+
MESSAGE,
102+
'm'
103+
) as Buffer;
104+
const rootPubKey = Buffer.from(userDkg.getCommonKeychain().slice(0, 64), 'hex');
105+
assert(ed25519.verify(sig, MESSAGE, rootPubKey), 'should verify under root public key from getCommonKeychain()');
106+
});
107+
108+
it('should not verify under the root public key when signing at a derived path (m/0/0)', function () {
109+
const sig = MPSUtil.executeTillRound(
110+
3,
111+
new EddsaMPSDsg.DSG(0),
112+
new EddsaMPSDsg.DSG(2),
113+
keySharesByIdx[0],
114+
keySharesByIdx[2],
115+
MESSAGE,
116+
'm/0/0'
117+
) as Buffer;
118+
assert.strictEqual(sig.length, 64, 'Derived path signature must be 64 bytes');
119+
const rootPubKey = Buffer.from(userDkg.getCommonKeychain().slice(0, 64), 'hex');
120+
assert(
121+
!ed25519.verify(sig, MESSAGE, rootPubKey),
122+
'derived-path signature should not verify under root public key'
123+
);
124+
});
125+
126+
it('should return message arrays (not a Buffer) for intermediate round 1', function () {
127+
const result = MPSUtil.executeTillRound(
128+
1,
129+
new EddsaMPSDsg.DSG(0),
130+
new EddsaMPSDsg.DSG(2),
131+
keySharesByIdx[0],
132+
keySharesByIdx[2],
133+
MESSAGE,
134+
'm'
135+
);
136+
assert(!Buffer.isBuffer(result), 'round 1 should return message arrays, not a Buffer');
137+
assert.strictEqual(result.length, 2, 'should contain message arrays for both parties');
138+
});
139+
140+
it('should return message arrays (not a Buffer) for intermediate round 2', function () {
141+
const result = MPSUtil.executeTillRound(
142+
2,
143+
new EddsaMPSDsg.DSG(0),
144+
new EddsaMPSDsg.DSG(2),
145+
keySharesByIdx[0],
146+
keySharesByIdx[2],
147+
MESSAGE,
148+
'm'
149+
);
150+
assert(!Buffer.isBuffer(result), 'round 2 should return message arrays, not a Buffer');
151+
assert.strictEqual(result.length, 2, 'should contain message arrays for both parties');
152+
});
153+
154+
it('should throw for round out of range', function () {
155+
const dsg1 = new EddsaMPSDsg.DSG(0);
156+
const dsg2 = new EddsaMPSDsg.DSG(2);
157+
assert.throws(
158+
() => MPSUtil.executeTillRound(0, dsg1, dsg2, keySharesByIdx[0], keySharesByIdx[2], MESSAGE, 'm'),
159+
/Invalid round number/
160+
);
161+
assert.throws(
162+
() => MPSUtil.executeTillRound(4, dsg1, dsg2, keySharesByIdx[0], keySharesByIdx[2], MESSAGE, 'm'),
163+
/Invalid round number/
164+
);
165+
});
166+
});
54167
});

0 commit comments

Comments
 (0)