Skip to content

Commit cf3cb10

Browse files
BitGo Agentbitgobot
authored andcommitted
feat(sdk-lib-mpc): EddsaRetrofitData type + DKG retrofit constructor + getFirstMessage routing
Add optional `retrofitData` parameter to the `DKG` constructor so parties can seed a retrofit DKG ceremony from their existing MPCv1 scalar instead of generating fresh key material. - Import and store `EddsaRetrofitData` on the `DKG` class instance. - Constructor gains a 4th optional param: `retrofitData?: EddsaRetrofitData`. - `getFirstMessage` branches on `this.retrofitData`: when set it calls `wasm.ed25519_dkg_round0_import` (ships in WCI-1217) passing the party's clamped scalar, aggregate public key, and chain code; otherwise it falls through to the existing `ed25519_dkg_round0_process` path. - Export `EddsaRetrofitData` as a named type from `eddsa-mps/index.ts` so callers can import it directly without going through `MPSTypes`. The `as any` cast on the wasm call-site is intentional and temporary: `ed25519_dkg_round0_import` will be typed once WCI-1217 lands. Ticket: WCI-1261 Session-Id: ac4bfdd3-68f2-464c-bb89-a871628f8093 Task-Id: bf6d94d3-a6fd-40c3-9c4a-efc7ffd08fdf
1 parent 9957029 commit cf3cb10

6 files changed

Lines changed: 115 additions & 13 deletions

File tree

modules/sdk-lib-mpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
]
3737
},
3838
"dependencies": {
39-
"@bitgo/wasm-mps": "1.10.0",
39+
"@bitgo/wasm-mps": "1.12.0",
4040
"@noble/curves": "1.8.1",
4141
"@silencelaboratories/dkls-wasm-ll-node": "1.2.0-pre.4",
4242
"@silencelaboratories/dkls-wasm-ll-web": "1.2.0-pre.4",

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { MsgState, Share } from '@bitgo/wasm-mps';
22
import { encode } from 'cbor-x';
33
import crypto from 'crypto';
4-
import { DeserializedMessage, DeserializedMessages, DkgState, EddsaReducedKeyShare } from './types';
4+
import { DeserializedMessage, DeserializedMessages, DkgState, EddsaReducedKeyShare, EddsaRetrofitData } from './types';
55

66
type NodeWasmer = typeof import('@bitgo/wasm-mps');
77
type WebWasmer = typeof import('@bitgo/wasm-mps/web');
@@ -44,13 +44,16 @@ export class DKG {
4444
private shareChaincode: Buffer | null = null;
4545
/** Lazily loaded WASM module */
4646
private wasmMps: WasmMps | null = null;
47+
/** Optional MPCv1 retrofit data; when set, round0 uses ed25519_dkg_round0_import */
48+
private retrofitData: EddsaRetrofitData | undefined;
4749

4850
protected dkgState: DkgState = DkgState.Uninitialized;
4951

50-
constructor(n: number, t: number, partyIdx: number) {
52+
constructor(n: number, t: number, partyIdx: number, retrofitData?: EddsaRetrofitData) {
5153
this.n = n;
5254
this.t = t;
5355
this.partyIdx = partyIdx;
56+
this.retrofitData = retrofitData;
5457
}
5558

5659
private async loadWasmMps(): Promise<void> {
@@ -124,7 +127,18 @@ export class DKG {
124127
const wasm = this.getWasmMps();
125128
let result: MsgState;
126129
try {
127-
result = wasm.ed25519_dkg_round0_process(this.partyIdx, this.decryptionKey!, this.otherPubKeys!, seed);
130+
if (this.retrofitData) {
131+
result = wasm.ed25519_dkg_round0_import(
132+
this.partyIdx,
133+
this.decryptionKey!,
134+
this.otherPubKeys!,
135+
Buffer.from(this.retrofitData.s_i_0, 'hex'),
136+
Buffer.from(this.retrofitData.expectedPk, 'hex'),
137+
Buffer.from(this.retrofitData.chainCode, 'hex'),
138+
);
139+
} else {
140+
result = wasm.ed25519_dkg_round0_process(this.partyIdx, this.decryptionKey!, this.otherPubKeys!, seed);
141+
}
128142
} catch (err) {
129143
throw new Error(`Error while creating the first message from party ${this.partyIdx}: ${err}`);
130144
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * as EddsaMPSDsg from './dsg';
33
export * as MPSUtil from './util';
44
export * as MPSTypes from './types';
55
export * as MPSComms from './commsLayer';
6+
export type { EddsaRetrofitData } from './types';

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import assert from 'assert';
33
import { x25519 } from '@noble/curves/ed25519';
44
import { DKG } from './dkg';
55
import { DSG } from './dsg';
6-
import { DeserializedMessages } from './types';
6+
import { DeserializedMessages, EddsaRetrofitData } from './types';
77

88
/**
99
* Concatenates multiple Uint8Array instances into a single Uint8Array
@@ -40,15 +40,18 @@ function validateSeed(seed?: EdDsaDKGPartySeed): EdDsaDKGPartySeed {
4040
export async function generateEdDsaDKGKeyShares(
4141
seedUser?: EdDsaDKGPartySeed,
4242
seedBackup?: EdDsaDKGPartySeed,
43-
seedBitgo?: EdDsaDKGPartySeed
43+
seedBitgo?: EdDsaDKGPartySeed,
44+
retrofitUser?: EddsaRetrofitData,
45+
retrofitBackup?: EddsaRetrofitData,
46+
retrofitBitgo?: EddsaRetrofitData
4447
): Promise<[DKG, DKG, DKG]> {
4548
const { encKey: userEncKey, dkgSeed: userDkgSeed } = validateSeed(seedUser);
4649
const { encKey: backupEncKey, dkgSeed: backupDkgSeed } = validateSeed(seedBackup);
4750
const { encKey: bitgoEncKey, dkgSeed: bitgoDkgSeed } = validateSeed(seedBitgo);
4851

49-
const user = new DKG(3, 2, 0);
50-
const backup = new DKG(3, 2, 1);
51-
const bitgo = new DKG(3, 2, 2);
52+
const user = new DKG(3, 2, 0, retrofitUser);
53+
const backup = new DKG(3, 2, 1, retrofitBackup);
54+
const bitgo = new DKG(3, 2, 2, retrofitBitgo);
5255

5356
const userKP = generateX25519Keypair(userEncKey);
5457
const backupKP = generateX25519Keypair(backupEncKey);

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from 'assert';
22
import crypto from 'crypto';
33
import { x25519 } from '@noble/curves/ed25519';
44
import { EddsaMPSDkg, MPSTypes } from '../../../../src/tss/eddsa-mps';
5+
import type { EddsaRetrofitData } from '../../../../src/tss/eddsa-mps';
56
import { generateEdDsaDKGKeyShares } from './util';
67

78
function makeKeypair(seed?: Buffer) {
@@ -311,4 +312,87 @@ describe('EdDSA MPS DKG', function () {
311312
}, /DKG session is complete. Exporting the session is not allowed./);
312313
});
313314
});
315+
316+
describe('Retrofit DKG (ed25519_dkg_round0_import)', function () {
317+
// Deterministic MPCv1-style scalars (32-byte LE, clamped Ed25519 scalars).
318+
// s_i_0 values are valid clamped Ed25519 scalars: bits 0-2 of byte 0 cleared,
319+
// bit 7 of byte 31 cleared, bit 6 of byte 31 set.
320+
const retrofitUser: EddsaRetrofitData = {
321+
s_i_0: '4866636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a49484748',
322+
expectedPk: '0000000000000000000000000000000000000000000000000000000000000000',
323+
chainCode: 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270',
324+
};
325+
const retrofitBackup: EddsaRetrofitData = {
326+
s_i_0: '4866636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a49484748',
327+
expectedPk: '0000000000000000000000000000000000000000000000000000000000000000',
328+
chainCode: 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270',
329+
};
330+
const retrofitBitgo: EddsaRetrofitData = {
331+
s_i_0: '4866636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a49484748',
332+
expectedPk: '0000000000000000000000000000000000000000000000000000000000000000',
333+
chainCode: 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270',
334+
};
335+
336+
it('should route getFirstMessage through ed25519_dkg_round0_import when retrofitData is set', async function () {
337+
const [user, backup, bitgo] = await generateEdDsaDKGKeyShares(
338+
undefined,
339+
undefined,
340+
undefined,
341+
retrofitUser,
342+
retrofitBackup,
343+
retrofitBitgo
344+
);
345+
346+
const userPk = user.getSharePublicKey().toString('hex');
347+
const backupPk = backup.getSharePublicKey().toString('hex');
348+
const bitgoPk = bitgo.getSharePublicKey().toString('hex');
349+
350+
assert.strictEqual(userPk, backupPk, 'user and backup must agree on public key after retrofit DKG');
351+
assert.strictEqual(backupPk, bitgoPk, 'backup and bitgo must agree on public key after retrofit DKG');
352+
assert.strictEqual(userPk.length, 64, 'public key must be 32 bytes (64 hex chars)');
353+
});
354+
355+
it('should produce a different public key from a fresh DKG with different seed material', async function () {
356+
const [retrofitUser1] = await generateEdDsaDKGKeyShares(
357+
undefined,
358+
undefined,
359+
undefined,
360+
retrofitUser,
361+
retrofitBackup,
362+
retrofitBitgo
363+
);
364+
const [freshUser] = await generateEdDsaDKGKeyShares();
365+
366+
assert.notStrictEqual(
367+
retrofitUser1.getSharePublicKey().toString('hex'),
368+
freshUser.getSharePublicKey().toString('hex'),
369+
'retrofit and fresh DKG should produce distinct public keys'
370+
);
371+
});
372+
373+
it('should be deterministic: same retrofitData produces same public key', async function () {
374+
const [run1] = await generateEdDsaDKGKeyShares(
375+
undefined,
376+
undefined,
377+
undefined,
378+
retrofitUser,
379+
retrofitBackup,
380+
retrofitBitgo
381+
);
382+
const [run2] = await generateEdDsaDKGKeyShares(
383+
undefined,
384+
undefined,
385+
undefined,
386+
retrofitUser,
387+
retrofitBackup,
388+
retrofitBitgo
389+
);
390+
391+
assert.strictEqual(
392+
run1.getSharePublicKey().toString('hex'),
393+
run2.getSharePublicKey().toString('hex'),
394+
'retrofit DKG must be deterministic: same inputs must produce same public key'
395+
);
396+
});
397+
});
314398
});

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,10 +1059,10 @@
10591059
resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.7.0.tgz"
10601060
integrity sha512-KoXavJvyDHlEN+sWcigbgxYJtdFaU7gS0EkYQbNH4npVjNlzo6rL6gwjyWbyOy7oEs65DhpJ9vY5kRbE/bKiTQ==
10611061

1062-
"@bitgo/wasm-mps@1.10.0":
1063-
version "1.10.0"
1064-
resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.10.0.tgz#df6a056247ce04c7d92369d257b659876e03261d"
1065-
integrity sha512-f42sMCyqqlaId3AtcvdpOfR+mOjAyVopCxCCAqW7wcTAQ8ZBS9rMGQIzTMiqFmZDBMWsAe+QHWA6XseIuzTVdQ==
1062+
"@bitgo/wasm-mps@1.12.0":
1063+
version "1.12.0"
1064+
resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.12.0.tgz#03f9fc8eaa25d3dcb5af61915bba890759110c65"
1065+
integrity sha512-rude1gS5ml/I/qpkCoeBwvMbveNQp4cWxWzh3wUO4SsXebJMHVmGmWE27EsTBDEiaYI470q5H4aI/oyAfENOUg==
10661066

10671067
"@bitgo/wasm-solana@^2.6.0":
10681068
version "2.6.0"

0 commit comments

Comments
 (0)