From ece015281cd637a5f9fe69835c9d5844fad97d41 Mon Sep 17 00:00:00 2001 From: Vibhav Simha G Date: Tue, 1 Sep 2026 12:11:13 +0530 Subject: [PATCH] test(sdk-lib-mpc): add EdDSA MPCv2 keyshare wire-format regression Check in a static DKG keyshare fixture (bincode-serialized Keyshare, 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 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 --- .../test/fixtures/eddsaMPCv2Keyshares.ts | 45 +++++++ .../test/unit/tss/eddsa/mpcv2Compatibility.ts | 122 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 modules/sdk-lib-mpc/test/fixtures/eddsaMPCv2Keyshares.ts create mode 100644 modules/sdk-lib-mpc/test/unit/tss/eddsa/mpcv2Compatibility.ts diff --git a/modules/sdk-lib-mpc/test/fixtures/eddsaMPCv2Keyshares.ts b/modules/sdk-lib-mpc/test/fixtures/eddsaMPCv2Keyshares.ts new file mode 100644 index 0000000000..cdc0df5128 --- /dev/null +++ b/modules/sdk-lib-mpc/test/fixtures/eddsaMPCv2Keyshares.ts @@ -0,0 +1,45 @@ +// Generated: 2026-09-04 +// @bitgo/wasm-mps version: 1.15.0 (pinned in modules/sdk-lib-mpc/package.json) +// Coin: EdDSA MPCv2 (3-party DKG, deterministic seeds) +// +// These are real bincode-serialized Keyshare blobs produced by +// @bitgo/wasm-mps at the version pinned in this repo. If the wire format changes, +// DSG round0 will fail to deserialize these bytes, making the regression test in +// test/unit/tss/eddsa/mpcv2Compatibility.ts the earliest signal of a breaking change. +// +// Regeneration procedure (deterministic; reproduces the exact bytes below while +// the pin stays at 1.15.0): +// - party i seed: sha256("wci-1476-fixture-party-" + i), i in {0, 1, 2} +// - per party: libsodium crypto_box_seed_keypair(seed) +// - DKG seed: a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270 +// - run the DKG via the sdk-lib-mpc DKG class (src/tss/eddsa-mps/dkg.ts): +// getFirstMessage(dkgSeed), then handleIncomingMessages for rounds 1-2. +// Party 0 -> userKeyShare, party 1 -> backupKeyShare, party 2 -> +// bitgoKeyShare; commonKeychain = hex(pk) + hex(chaincode); party 0's +// getReducedKeyShare() -> userReducedKeyShare. +// Same fixtures as hsm-api PR #1262 and BitGoWASM PR #376. + +export const EDDSA_MPCV2_FIXTURES = { + commonKeychain: + '4d085a45375f3308d93f9a7de21bd6edd77a7a5d7a02cd0c1a2f4ba4560bd76a926021c9cd6712e0e52ccf7f6867eb0ebc8889f335d34f21dd5e36b2db5cb4fe', + + // Opaque bincode-serialized Keyshare from @bitgo/wasm-mps + userKeyShare: Buffer.from( + '0203007c01f56048ff0c0056edac6cc44d54c6d95fe68d072d1c1fbad9c4f3fac9ec054d085a45375f3308d93f9a7de21bd6edd77a7a5d7a02cd0c1a2f4ba4560bd76a6058f5d7601f380c8419e6fce20b8fbd2ec63ab06b5f4b81cab3959cd4e01433452df0e73fb57b332be5bc0bbb4cd34585494614923309be8a844125ed773af2456613b78a6339153b12a20152c055f19ba02283c84c4fe068d760bfcfc7e1d620c182a8d5159b5122940b3063f76d3f713e9e01a06f6e770502b7b8e97fe03a3b00926021c9cd6712e0e52ccf7f6867eb0ebc8889f335d34f21dd5e36b2db5cb4fe', + 'hex' + ), + backupKeyShare: Buffer.from( + '020301ee23eba58bf28bfe1c67150ddb4bac0fb057a74e227eadc7eaf8c237f36ca0044d085a45375f3308d93f9a7de21bd6edd77a7a5d7a02cd0c1a2f4ba4560bd76a6058f5d7601f380c8419e6fce20b8fbd2ec63ab06b5f4b81cab3959cd4e01433452df0e73fb57b332be5bc0bbb4cd34585494614923309be8a844125ed773af2456613b78a6339153b12a20152c055f19ba02283c84c4fe068d760bfcfc7e1d620c182a8d5159b5122940b3063f76d3f713e9e01a06f6e770502b7b8e97fe03a3b00926021c9cd6712e0e52ccf7f6867eb0ebc8889f335d34f21dd5e36b2db5cb4fe', + 'hex' + ), + bitgoKeyShare: Buffer.from( + '0203026046e1eacee50afde3e07dadf1490459864f680f3dcf3e701b18c17beb0f54034d085a45375f3308d93f9a7de21bd6edd77a7a5d7a02cd0c1a2f4ba4560bd76a6058f5d7601f380c8419e6fce20b8fbd2ec63ab06b5f4b81cab3959cd4e01433452df0e73fb57b332be5bc0bbb4cd34585494614923309be8a844125ed773af2456613b78a6339153b12a20152c055f19ba02283c84c4fe068d760bfcfc7e1d620c182a8d5159b5122940b3063f76d3f713e9e01a06f6e770502b7b8e97fe03a3b00926021c9cd6712e0e52ccf7f6867eb0ebc8889f335d34f21dd5e36b2db5cb4fe', + 'hex' + ), + + // CBOR-encoded EddsaReducedKeyShare for the user party + userReducedKeyShare: Buffer.from( + 'b90003686b6579536861726598e5020300187c0118f51860184818ff0c00185618ed18ac186c18c4184d185418c618d9185f18e6188d07182d181c181f18ba18d918c418f318fa18c918ec05184d08185a18451837185f18330818d9183f189a187d18e2181b18d618ed18d7187a187a185d187a0218cd0c181a182f184b18a418560b18d7186a1860185818f518d71860181f18380c1884181918e618fc18e20b188f18bd182e18c6183a18b0186b185f184b188118ca18b31895189c18d418e01418331845182d18f018e7183f18b5187b1833182b18e518bc0b18bb184c18d3184518851849184614189218330918be188a18841841182518ed1877183a18f2184518661318b7188a1863183915183b1218a201185218c0185518f1189b18a01822188318c8184c184f18e0186818d7186018bf18cf18c718e118d6182018c1188218a818d515189b1851182218940b1830186318f7186d183f1871183e189e0118a0186f186e1877050218b718b818e9187f18e0183a183b0018921860182118c918cd18671218e018e5182c18cf187f1868186718eb0e18bc1888188918f3183518d3184f182118dd185e183618b218db185c18b418fe637075629820184d08185a18451837185f18330818d9183f189a187d18e2181b18d618ed18d7187a187a185d187a0218cd0c181a182f184b18a418560b18d7186a6d726f6f74436861696e436f6465982018921860182118c918cd18671218e018e5182c18cf187f1868186718eb0e18bc1888188918f3183518d3184f182118dd185e183618b218db185c18b418fe', + 'hex' + ), +} as const; diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/mpcv2Compatibility.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/mpcv2Compatibility.ts new file mode 100644 index 0000000000..2eb8ff7f05 --- /dev/null +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/mpcv2Compatibility.ts @@ -0,0 +1,122 @@ +/** + * Wire-format compatibility regression tests for EdDSA MPCv2 keyshares. + * + * These tests load *frozen* keyshare bytes captured at a specific @bitgo/sdk-lib-mpc + * version. If @bitgo/wasm-mps changes its bincode layout, DSG round 0 will throw + * while deserializing the fixture bytes — the earliest possible signal of a + * breaking change before it reaches production. + */ +import assert from 'assert'; +import crypto from 'crypto'; +import { ed25519 } from '@noble/curves/ed25519'; +import { EddsaMPSDsg, MPSTypes, MPSUtil } from '../../../../src/tss/eddsa-mps'; +import { EDDSA_MPCV2_FIXTURES } from '../../../fixtures/eddsaMPCv2Keyshares'; + +const MESSAGE = Buffer.from('WCI-1476 EdDSA MPCv2 wire-format regression sentinel'); + +describe('EdDSA MPCv2 wire-format compatibility', function () { + it('should deserialize frozen user keyshare without error (bincode format guard)', async function () { + const dsg = new EddsaMPSDsg.DSG(0); + await dsg.initDsg(EDDSA_MPCV2_FIXTURES.userKeyShare, MESSAGE, 'm', 2); + const msg = dsg.getFirstMessage(); + assert(msg.payload.length > 0, 'DSG round 0 must produce a non-empty message'); + }); + + it('should deserialize frozen backup keyshare without error', async function () { + const dsg = new EddsaMPSDsg.DSG(1); + await dsg.initDsg(EDDSA_MPCV2_FIXTURES.backupKeyShare, MESSAGE, 'm', 2); + const msg = dsg.getFirstMessage(); + assert(msg.payload.length > 0, 'Backup DSG round 0 must produce a non-empty message'); + }); + + it('should deserialize frozen bitgo keyshare without error', async function () { + const dsg = new EddsaMPSDsg.DSG(2); + await dsg.initDsg(EDDSA_MPCV2_FIXTURES.bitgoKeyShare, MESSAGE, 'm', 0); + const msg = dsg.getFirstMessage(); + assert(msg.payload.length > 0, 'BitGo DSG round 0 must produce a non-empty message'); + }); + + it('should produce a valid signature from frozen user+bitgo keyshares', async function () { + const sig = (await MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + EDDSA_MPCV2_FIXTURES.userKeyShare, + EDDSA_MPCV2_FIXTURES.bitgoKeyShare, + MESSAGE, + 'm' + )) as Buffer; + + assert.strictEqual(sig.length, 64, 'Signature must be 64 bytes'); + + const pubKeyHex = EDDSA_MPCV2_FIXTURES.commonKeychain.slice(0, 64); + const pubKey = Buffer.from(pubKeyHex, 'hex'); + assert(ed25519.verify(sig, MESSAGE, pubKey), 'Signature must verify under the frozen commonKeychain public key'); + }); + + it('should produce a valid signature from frozen user+backup keyshares', async function () { + const sig = (await MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(1), + EDDSA_MPCV2_FIXTURES.userKeyShare, + EDDSA_MPCV2_FIXTURES.backupKeyShare, + MESSAGE, + 'm' + )) as Buffer; + + const pubKeyHex = EDDSA_MPCV2_FIXTURES.commonKeychain.slice(0, 64); + const pubKey = Buffer.from(pubKeyHex, 'hex'); + assert(ed25519.verify(sig, MESSAGE, pubKey), 'Signature must verify under the frozen commonKeychain public key'); + }); + + it('should decode frozen reducedKeyShare and match commonKeychain fields', function () { + const decoded = MPSTypes.getDecodedReducedKeyShare(EDDSA_MPCV2_FIXTURES.userReducedKeyShare); + + const pubHex = EDDSA_MPCV2_FIXTURES.commonKeychain.slice(0, 64); + const chaincodeHex = EDDSA_MPCV2_FIXTURES.commonKeychain.slice(64); + + assert.strictEqual( + Buffer.from(decoded.pub).toString('hex'), + pubHex, + 'Decoded pub must match commonKeychain pubkey' + ); + assert.strictEqual( + Buffer.from(decoded.rootChainCode).toString('hex'), + chaincodeHex, + 'Decoded rootChainCode must match commonKeychain chaincode' + ); + assert(decoded.keyShare.length > 0, 'keyShare in reducedKeyShare must be non-empty'); + }); + + it('should reject randomised bytes as a keyshare (guard validation)', async function () { + const randomBytes = Buffer.from(crypto.randomBytes(EDDSA_MPCV2_FIXTURES.userKeyShare.length)); + const dsg = new EddsaMPSDsg.DSG(0); + await dsg.initDsg(randomBytes, MESSAGE, 'm', 2); + assert.throws( + () => dsg.getFirstMessage(), + /Error while creating/, + 'DSG round 0 must reject random bytes that are not a valid bincode keyshare' + ); + }); + + it('should produce a verifiable signature at a derived path from frozen keyshares', async function () { + const sig = (await MPSUtil.executeTillRound( + 3, + new EddsaMPSDsg.DSG(0), + new EddsaMPSDsg.DSG(2), + EDDSA_MPCV2_FIXTURES.userKeyShare, + EDDSA_MPCV2_FIXTURES.bitgoKeyShare, + MESSAGE, + 'm/0/1' + )) as Buffer; + + assert.strictEqual(sig.length, 64, 'Derived-path signature must be 64 bytes'); + + const rootPubKey = Buffer.from(EDDSA_MPCV2_FIXTURES.commonKeychain.slice(0, 64), 'hex'); + // A derived-path signature must NOT verify under the root public key — + // it verifies under the child key derived at m/0/1. This confirms the + // WASM applied key derivation rather than signing with the root key. + assert(!ed25519.verify(sig, MESSAGE, rootPubKey), 'derived-path signature must not verify under root public key'); + }); +});