Skip to content

Commit b2b22ab

Browse files
committed
test(sdk-lib-mpc): add EdDSA MPCv2 keyshare wire-format regression
Check in a static DKG keyshare fixture (bincode-serialized Keyshare<EdwardsPoint>, generated 2026-09-01 from @bitgo/sdk-lib-mpc @10.18.0) and a regression test that loads it through DSG and verifies the resulting Ed25519 signature. If wasm-mps changes its bincode layout or the multi-party-schnorr Keyshare<G> struct gains new fields via Cargo feature unification, DSG round 0 deserialization fails and CI catches the breaking change before it reaches production. Ticket: WCI-1476
1 parent 16b56f2 commit b2b22ab

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Generated: 2026-09-01
2+
// @bitgo/sdk-lib-mpc version: 10.18.0
3+
// Coin: EdDSA MPCv2 (3-party DKG, deterministic seeds)
4+
//
5+
// These are real bincode-serialized Keyshare<EdwardsPoint> blobs produced by
6+
// @bitgo/wasm-mps at the version pinned in sdk-lib-mpc at generation time.
7+
// If the wire format changes, DSG round0 will fail to deserialize these bytes,
8+
// making the regression test below the earliest signal of a breaking change.
9+
10+
export const EDDSA_MPCV2_FIXTURES = {
11+
commonKeychain:
12+
'6baa8e5ff9598564971770ecf5e1e9015f54ba614d39ac891eaf38d71c8eb46f92cd4c5f1d950f933f83f3f5a5cec3b19ac527e099df3435708d5aaeb0d5840d',
13+
14+
// Opaque bincode-serialized Keyshare<EdwardsPoint> from @bitgo/wasm-mps
15+
userKeyShare: Buffer.from(
16+
'02030029e955f0a2b4eda4f79c002e53d68ff4d2ba4132e0a373a7152e2c23b1293b0a6baa8e5ff9598564971770ecf5e1e9015f54ba614d39ac891eaf38d71c8eb46f616f4774f13e0351b6a5e6de9d67bcd2d6904f4e538337b05b0771174daefd320092cd4c5f1d950f933f83f3f5a5cec3b19ac527e099df3435708d5aaeb0d5840d01fa4ed5a0d728ae2391b0b4e40091b03f1470b8a363bc04d73484e53e87a453',
17+
'hex'
18+
),
19+
backupKeyShare: Buffer.from(
20+
'020301914acb96aa584e61b5767fed4fde813dcd211ca638bc67cb2b0744ee773952006baa8e5ff9598564971770ecf5e1e9015f54ba614d39ac891eaf38d71c8eb46f616f4774f13e0351b6a5e6de9d67bcd2d6904f4e538337b05b0771174daefd320092cd4c5f1d950f933f83f3f5a5cec3b19ac527e099df3435708d5aaeb0d5840d01fa4ed5a0d728ae2391b0b4e40091b03f1470b8a363bc04d73484e53e87a453',
21+
'hex'
22+
),
23+
bitgoKeyShare: Buffer.from(
24+
'020302e67f369acc5fc17549edf54f2be0529bc788f61991d45bef41e05bb93e4969066baa8e5ff9598564971770ecf5e1e9015f54ba614d39ac891eaf38d71c8eb46f616f4774f13e0351b6a5e6de9d67bcd2d6904f4e538337b05b0771174daefd320092cd4c5f1d950f933f83f3f5a5cec3b19ac527e099df3435708d5aaeb0d5840d01fa4ed5a0d728ae2391b0b4e40091b03f1470b8a363bc04d73484e53e87a453',
25+
'hex'
26+
),
27+
28+
// CBOR-encoded EddsaReducedKeyShare for the user party
29+
userReducedKeyShare: Buffer.from(
30+
'b90003686b6579536861726598a4020300182918e9185518f018a218b418ed18a418f7189c00182e185318d6188f18f418d218ba1841183218e018a3187318a715182e182c182318b11829183b0a186b18aa188e185f18f9185918851864189717187018ec18f518e118e901185f185418ba1861184d183918ac1889181e18af183818d7181c188e18b4186f1861186f1847187418f1183e03185118b618a518e618de189d186718bc18d218d61890184f184e18531883183718b0185b07187117184d18ae18fd183200189218cd184c185f181d18950f1893183f188318f318f518a518ce18c318b1189a18c5182718e0189918df183418351870188d185a18ae18b018d518840d0118fa184e18d518a018d7182818ae1823189118b018b418e400189118b0183f14187018b818a3186318bc0418d71834188418e5183e188718a41853637075629820186b18aa188e185f18f9185918851864189717187018ec18f518e118e901185f185418ba1861184d183918ac1889181e18af183818d7181c188e18b4186f6d726f6f74436861696e436f64659820189218cd184c185f181d18950f1893183f188318f318f518a518ce18c318b1189a18c5182718e0189918df183418351870188d185a18ae18b018d518840d',
31+
'hex'
32+
),
33+
} as const;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Wire-format compatibility regression tests for EdDSA MPCv2 keyshares.
3+
*
4+
* These tests load *frozen* keyshare bytes captured at a specific @bitgo/sdk-lib-mpc
5+
* version. If @bitgo/wasm-mps changes its bincode layout, DSG round 0 will throw
6+
* while deserializing the fixture bytes — the earliest possible signal of a
7+
* breaking change before it reaches production.
8+
*/
9+
import assert from 'assert';
10+
import crypto from 'crypto';
11+
import { ed25519 } from '@noble/curves/ed25519';
12+
import { EddsaMPSDsg, MPSTypes, MPSUtil } from '../../../../src/tss/eddsa-mps';
13+
import { EDDSA_MPCV2_FIXTURES } from '../../../fixtures/eddsaMPCv2Keyshares';
14+
15+
const MESSAGE = Buffer.from('WCI-1476 EdDSA MPCv2 wire-format regression sentinel');
16+
17+
describe('EdDSA MPCv2 wire-format compatibility', function () {
18+
it('should deserialize frozen user keyshare without error (bincode format guard)', async function () {
19+
const dsg = new EddsaMPSDsg.DSG(0);
20+
await dsg.initDsg(EDDSA_MPCV2_FIXTURES.userKeyShare, MESSAGE, 'm', 2);
21+
const msg = dsg.getFirstMessage();
22+
assert(msg.payload.length > 0, 'DSG round 0 must produce a non-empty message');
23+
});
24+
25+
it('should deserialize frozen backup keyshare without error', async function () {
26+
const dsg = new EddsaMPSDsg.DSG(1);
27+
await dsg.initDsg(EDDSA_MPCV2_FIXTURES.backupKeyShare, MESSAGE, 'm', 2);
28+
const msg = dsg.getFirstMessage();
29+
assert(msg.payload.length > 0, 'Backup DSG round 0 must produce a non-empty message');
30+
});
31+
32+
it('should deserialize frozen bitgo keyshare without error', async function () {
33+
const dsg = new EddsaMPSDsg.DSG(2);
34+
await dsg.initDsg(EDDSA_MPCV2_FIXTURES.bitgoKeyShare, MESSAGE, 'm', 0);
35+
const msg = dsg.getFirstMessage();
36+
assert(msg.payload.length > 0, 'BitGo DSG round 0 must produce a non-empty message');
37+
});
38+
39+
it('should produce a valid signature from frozen user+bitgo keyshares', async function () {
40+
const sig = (await MPSUtil.executeTillRound(
41+
3,
42+
new EddsaMPSDsg.DSG(0),
43+
new EddsaMPSDsg.DSG(2),
44+
EDDSA_MPCV2_FIXTURES.userKeyShare,
45+
EDDSA_MPCV2_FIXTURES.bitgoKeyShare,
46+
MESSAGE,
47+
'm'
48+
)) as Buffer;
49+
50+
assert.strictEqual(sig.length, 64, 'Signature must be 64 bytes');
51+
52+
const pubKeyHex = EDDSA_MPCV2_FIXTURES.commonKeychain.slice(0, 64);
53+
const pubKey = Buffer.from(pubKeyHex, 'hex');
54+
assert(ed25519.verify(sig, MESSAGE, pubKey), 'Signature must verify under the frozen commonKeychain public key');
55+
});
56+
57+
it('should produce a valid signature from frozen user+backup keyshares', async function () {
58+
const sig = (await MPSUtil.executeTillRound(
59+
3,
60+
new EddsaMPSDsg.DSG(0),
61+
new EddsaMPSDsg.DSG(1),
62+
EDDSA_MPCV2_FIXTURES.userKeyShare,
63+
EDDSA_MPCV2_FIXTURES.backupKeyShare,
64+
MESSAGE,
65+
'm'
66+
)) as Buffer;
67+
68+
const pubKeyHex = EDDSA_MPCV2_FIXTURES.commonKeychain.slice(0, 64);
69+
const pubKey = Buffer.from(pubKeyHex, 'hex');
70+
assert(ed25519.verify(sig, MESSAGE, pubKey), 'Signature must verify under the frozen commonKeychain public key');
71+
});
72+
73+
it('should decode frozen reducedKeyShare and match commonKeychain fields', function () {
74+
const decoded = MPSTypes.getDecodedReducedKeyShare(EDDSA_MPCV2_FIXTURES.userReducedKeyShare);
75+
76+
const pubHex = EDDSA_MPCV2_FIXTURES.commonKeychain.slice(0, 64);
77+
const chaincodeHex = EDDSA_MPCV2_FIXTURES.commonKeychain.slice(64);
78+
79+
assert.strictEqual(
80+
Buffer.from(decoded.pub).toString('hex'),
81+
pubHex,
82+
'Decoded pub must match commonKeychain pubkey'
83+
);
84+
assert.strictEqual(
85+
Buffer.from(decoded.rootChainCode).toString('hex'),
86+
chaincodeHex,
87+
'Decoded rootChainCode must match commonKeychain chaincode'
88+
);
89+
assert(decoded.keyShare.length > 0, 'keyShare in reducedKeyShare must be non-empty');
90+
});
91+
92+
it('should reject randomised bytes as a keyshare (guard validation)', async function () {
93+
const randomBytes = Buffer.from(crypto.randomBytes(EDDSA_MPCV2_FIXTURES.userKeyShare.length));
94+
const dsg = new EddsaMPSDsg.DSG(0);
95+
await dsg.initDsg(randomBytes, MESSAGE, 'm', 2);
96+
assert.throws(
97+
() => dsg.getFirstMessage(),
98+
/Error while creating/,
99+
'DSG round 0 must reject random bytes that are not a valid bincode keyshare'
100+
);
101+
});
102+
103+
it('should produce a valid signature at a derived path from frozen keyshares', async function () {
104+
const sig = (await MPSUtil.executeTillRound(
105+
3,
106+
new EddsaMPSDsg.DSG(0),
107+
new EddsaMPSDsg.DSG(2),
108+
EDDSA_MPCV2_FIXTURES.userKeyShare,
109+
EDDSA_MPCV2_FIXTURES.bitgoKeyShare,
110+
MESSAGE,
111+
'm/0/1'
112+
)) as Buffer;
113+
114+
assert.strictEqual(sig.length, 64, 'Derived-path signature must be 64 bytes');
115+
});
116+
});

0 commit comments

Comments
 (0)