Skip to content

Commit 7c3783d

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/dsg.ts covering full sign flow with real DKG key shares, root-key signature verification via getCommonKeychain(), derived-path (m/0/0) key isolation check, intermediate round type assertions, and out-of-range round error handling Ticket: WCI-386
1 parent c1af298 commit 7c3783d

3 files changed

Lines changed: 123 additions & 1 deletion

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: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'assert';
22
import { ed25519 } from '@noble/curves/ed25519';
3-
import { EddsaMPSDkg, EddsaMPSDsg, MPSTypes } from '../../../../src/tss/eddsa-mps';
3+
import { EddsaMPSDkg, EddsaMPSDsg, MPSTypes, MPSUtil } from '../../../../src/tss/eddsa-mps';
44
import { generateEdDsaDKGKeyShares, runEdDsaDSG } from './util';
55

66
const MESSAGE = Buffer.from('The Times 03/Jan/2009 Chancellor on brink of second bailout for banks');
@@ -203,6 +203,74 @@ describe('EdDSA MPS DSG', function () {
203203
});
204204
});
205205

206+
describe('executeTillRound', function () {
207+
it('should return 64-byte signature verifying under root public key from getCommonKeychain()', function () {
208+
const party1Dsg = new EddsaMPSDsg.DSG(0);
209+
const party2Dsg = new EddsaMPSDsg.DSG(2);
210+
const result = MPSUtil.executeTillRound(3, party1Dsg, party2Dsg, userKeyShare, bitgoKeyShare, MESSAGE, 'm');
211+
assert(Buffer.isBuffer(result), 'round 3 should return a Buffer');
212+
const sig = result as Buffer;
213+
assert.strictEqual(sig.length, 64, 'Signature must be 64 bytes');
214+
const rootPubKey = Buffer.from(userDkg.getCommonKeychain().slice(0, 64), 'hex');
215+
assert(
216+
ed25519.verify(sig, MESSAGE, rootPubKey),
217+
'Signature should verify under root public key from getCommonKeychain()'
218+
);
219+
});
220+
221+
it('should sign at m/0/0 using a derived key distinct from the root key', function () {
222+
const party1Dsg = new EddsaMPSDsg.DSG(0);
223+
const party2Dsg = new EddsaMPSDsg.DSG(2);
224+
const sig = MPSUtil.executeTillRound(
225+
3,
226+
party1Dsg,
227+
party2Dsg,
228+
userKeyShare,
229+
bitgoKeyShare,
230+
MESSAGE,
231+
'm/0/0'
232+
) as Buffer;
233+
assert.strictEqual(sig.length, 64, 'Derived path signature must be 64 bytes');
234+
235+
// A derived path uses a different signing key, so the signature must NOT
236+
// verify under the root public key.
237+
const rootPubKey = Buffer.from(userDkg.getCommonKeychain().slice(0, 64), 'hex');
238+
assert(
239+
!ed25519.verify(sig, MESSAGE, rootPubKey),
240+
'Derived-path signature should not verify under the root public key'
241+
);
242+
});
243+
244+
it('should return message arrays (not a Buffer) for intermediate round 1', function () {
245+
const party1Dsg = new EddsaMPSDsg.DSG(0);
246+
const party2Dsg = new EddsaMPSDsg.DSG(2);
247+
const result = MPSUtil.executeTillRound(1, party1Dsg, party2Dsg, userKeyShare, bitgoKeyShare, MESSAGE, 'm');
248+
assert(!Buffer.isBuffer(result), 'round 1 should return message arrays, not a signature Buffer');
249+
assert.strictEqual(result.length, 2, 'should have message arrays for both parties');
250+
});
251+
252+
it('should return message arrays (not a Buffer) for intermediate round 2', function () {
253+
const party1Dsg = new EddsaMPSDsg.DSG(0);
254+
const party2Dsg = new EddsaMPSDsg.DSG(2);
255+
const result = MPSUtil.executeTillRound(2, party1Dsg, party2Dsg, userKeyShare, bitgoKeyShare, MESSAGE, 'm');
256+
assert(!Buffer.isBuffer(result), 'round 2 should return message arrays, not a signature Buffer');
257+
assert.strictEqual(result.length, 2, 'should have message arrays for both parties');
258+
});
259+
260+
it('should throw for round out of range', function () {
261+
const party1Dsg = new EddsaMPSDsg.DSG(0);
262+
const party2Dsg = new EddsaMPSDsg.DSG(2);
263+
assert.throws(
264+
() => MPSUtil.executeTillRound(0, party1Dsg, party2Dsg, userKeyShare, bitgoKeyShare, MESSAGE, 'm'),
265+
/Invalid round number/
266+
);
267+
assert.throws(
268+
() => MPSUtil.executeTillRound(4, party1Dsg, party2Dsg, userKeyShare, bitgoKeyShare, MESSAGE, 'm'),
269+
/Invalid round number/
270+
);
271+
});
272+
});
273+
206274
describe('Session Management', function () {
207275
it('should export and restore DSG session and continue protocol to a valid signature', function () {
208276
const dsgA = new EddsaMPSDsg.DSG(0);

0 commit comments

Comments
 (0)