Skip to content

Commit b41cf96

Browse files
Merge pull request #9670 from BitGo/WCI-1570
feat(sdk-core): wire RedPallas MPCv2 into MPC dispatch
2 parents f9e04f9 + 29d050f commit b41cf96

6 files changed

Lines changed: 465 additions & 7 deletions

File tree

modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IPendingApprovals } from '../pendingApproval';
88
import { InitiateRecoveryOptions } from '../recovery';
99
import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa';
1010
import EddsaUtils, { EddsaMPCv2Utils, PrebuildTransactionWithIntentOptions, TxRequest } from '../utils/tss/eddsa';
11+
import { RedpallasMPCv2Utils } from '../utils/tss/redpallas';
1112
import { CreateAddressFormat, CustomSigningFunction, IWallet, IWallets, Memo, Wallet, WalletData } from '../wallet';
1213

1314
import { TokenEnablement } from '@bitgo/public-types';
@@ -364,7 +365,7 @@ export interface ExtraPrebuildParamsOptions {
364365
export interface PresignTransactionOptions {
365366
txPrebuild?: TransactionPrebuild;
366367
walletData: WalletData;
367-
tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | undefined;
368+
tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | RedpallasMPCv2Utils | undefined;
368369
[index: string]: unknown;
369370
}
370371

@@ -628,11 +629,14 @@ export interface MessagePrep {
628629
}
629630

630631
/**
631-
* 'redpallas' is a DKG-only MPC algorithm (no signing support in this SDK) used for the
632-
* Zcash Orchard shielded pool. It is additive: existing coins never return it from
633-
* `getMPCAlgorithm()` unless explicitly implemented to do so, so this does not change
634-
* behavior for any existing ECDSA/EdDSA coin or for ZEC's existing transparent
635-
* (secp256k1) multisig/TSS flows.
632+
* 'redpallas' is the MPC algorithm used for the Zcash Orchard shielded pool (RedPallas /
633+
* "Ironwood"). Today it only supports DKG (key generation) via MPCv2 in this SDK - there is no
634+
* online self-custody DSG (transaction signing) entrypoint yet, though the underlying
635+
* signature-share primitives (`bitgo/tss/redpallas`) and the `sendSignatureShareV2` MPCv2
636+
* request type exist as groundwork for a future custodial/cold (SMC/OVC) DSG signing flow.
637+
* It is additive: existing coins never return it from `getMPCAlgorithm()` unless explicitly
638+
* implemented to do so, so this does not change behavior for any existing ECDSA/EdDSA coin or
639+
* for ZEC's existing transparent (secp256k1) multisig/TSS flows.
636640
*/
637641
export type MPCAlgorithm = 'ecdsa' | 'eddsa' | 'redpallas';
638642

modules/sdk-core/src/bitgo/tss/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ export async function sendSignatureShareV2(
158158
type = 'ecdsaMpcV2';
159159
} else if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'eddsa') {
160160
type = 'eddsaMpcV2';
161+
} else if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'redpallas') {
162+
type = 'redpallasMpcV2';
161163
} else if (multisigTypeVersion === undefined && mpcAlgorithm === 'eddsa') {
162164
type = 'eddsaMpcV1';
163165
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import * as openpgp from 'openpgp';
2+
import { RedPallasMPSComms, RedPallasMPSTypes } from '@bitgo/sdk-lib-mpc';
3+
import {
4+
RedpallasMPCv2SignatureShareRound1Input,
5+
RedpallasMPCv2SignatureShareRound1Output,
6+
RedpallasMPCv2SignatureShareRound2Input,
7+
RedpallasMPCv2SignatureShareRound2Output,
8+
RedpallasMPCv2SignatureShareRound3Input,
9+
RedpallasMPCv2SignatureShareRound3Output,
10+
} from '@bitgo/public-types';
11+
import { SignatureShareRecord, SignatureShareType } from '../../utils/tss/baseTypes';
12+
import { MPCv2PartiesEnum } from '../../utils/tss/ecdsa/typesMPCv2';
13+
14+
type SignerPartyId = MPCv2PartiesEnum.USER | MPCv2PartiesEnum.BACKUP;
15+
16+
function partyIdToSignatureShareType(partyId: MPCv2PartiesEnum): SignatureShareType {
17+
switch (partyId) {
18+
case MPCv2PartiesEnum.USER:
19+
return SignatureShareType.USER;
20+
case MPCv2PartiesEnum.BACKUP:
21+
return SignatureShareType.BACKUP;
22+
case MPCv2PartiesEnum.BITGO:
23+
return SignatureShareType.BITGO;
24+
}
25+
}
26+
27+
/**
28+
* RedPallas MPS DSG signature-share helpers.
29+
*
30+
* Groundwork for a future custodial/cold (SMC/OVC) DSG signing flow - not yet wired up to any
31+
* caller in this SDK. Mirrors `../eddsa/eddsaMPCv2.ts` (same 3-round shape, same PGP-signed-
32+
* message envelope), but kept as an independent copy - built on `RedPallasMPSComms` - so
33+
* RedPallas MPS never depends on the EdDSA MPS module, and vice versa.
34+
*/
35+
36+
/**
37+
* Builds the round-1 signature share record.
38+
*
39+
* PGP-signs the WASM round-0 broadcast message with the signer's ephemeral key and
40+
* wraps it into a SignatureShareRecord ready for `sendSignatureShareV2`.
41+
*/
42+
export async function getSignatureShareRoundOne(
43+
userMsg1: RedPallasMPSTypes.DeserializedMessage,
44+
userGpgPrivKey: openpgp.PrivateKey,
45+
partyId: SignerPartyId = MPCv2PartiesEnum.USER,
46+
otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
47+
): Promise<SignatureShareRecord> {
48+
const signedMsg1 = await RedPallasMPSComms.detachSignMpsMessage(Buffer.from(userMsg1.payload), userGpgPrivKey);
49+
const share: RedpallasMPCv2SignatureShareRound1Input = {
50+
type: 'round1Input',
51+
data: { msg1: signedMsg1 },
52+
};
53+
return {
54+
from: partyIdToSignatureShareType(partyId),
55+
to: partyIdToSignatureShareType(otherSignerPartyId),
56+
share: JSON.stringify(share),
57+
};
58+
}
59+
60+
/**
61+
* Verifies the peer's round-1 PGP signature and returns the raw deserialized
62+
* message ready for `RedPallasDSG.handleIncomingMessages`.
63+
*/
64+
export async function verifyPeerMessageRoundOne(
65+
parsedRound1Output: RedpallasMPCv2SignatureShareRound1Output,
66+
peerGpgKey: openpgp.Key,
67+
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
68+
): Promise<RedPallasMPSTypes.DeserializedMessage> {
69+
const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsedRound1Output.data.msg1, peerGpgKey);
70+
return {
71+
from: peerPartyId,
72+
payload: new Uint8Array(rawBytes),
73+
};
74+
}
75+
76+
/**
77+
* Builds the round-2 signature share record.
78+
*/
79+
export async function getSignatureShareRoundTwo(
80+
userMsg2: RedPallasMPSTypes.DeserializedMessage,
81+
userGpgPrivKey: openpgp.PrivateKey,
82+
partyId: SignerPartyId = MPCv2PartiesEnum.USER,
83+
otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
84+
): Promise<SignatureShareRecord> {
85+
const signedMsg2 = await RedPallasMPSComms.detachSignMpsMessage(Buffer.from(userMsg2.payload), userGpgPrivKey);
86+
const share: RedpallasMPCv2SignatureShareRound2Input = {
87+
type: 'round2Input',
88+
data: { msg2: signedMsg2 },
89+
};
90+
return {
91+
from: partyIdToSignatureShareType(partyId),
92+
to: partyIdToSignatureShareType(otherSignerPartyId),
93+
share: JSON.stringify(share),
94+
};
95+
}
96+
97+
/**
98+
* Verifies the peer's round-2 PGP signature and returns the raw deserialized
99+
* message ready for `RedPallasDSG.handleIncomingMessages`.
100+
*/
101+
export async function verifyPeerMessageRoundTwo(
102+
parsedRound2Output: RedpallasMPCv2SignatureShareRound2Output,
103+
peerGpgKey: openpgp.Key,
104+
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
105+
): Promise<RedPallasMPSTypes.DeserializedMessage> {
106+
const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsedRound2Output.data.msg2, peerGpgKey);
107+
return {
108+
from: peerPartyId,
109+
payload: new Uint8Array(rawBytes),
110+
};
111+
}
112+
113+
/**
114+
* Verifies the peer's round-3 PGP signature and returns the raw deserialized
115+
* message ready for `RedPallasDSG.handleIncomingMessages`.
116+
*/
117+
export async function verifyPeerMessageRoundThree(
118+
parsedRound3Output: RedpallasMPCv2SignatureShareRound3Output,
119+
peerGpgKey: openpgp.Key,
120+
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
121+
): Promise<RedPallasMPSTypes.DeserializedMessage> {
122+
const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsedRound3Output.data.msg3, peerGpgKey);
123+
return { from: peerPartyId, payload: new Uint8Array(rawBytes) };
124+
}
125+
126+
/**
127+
* Builds the round-3 signature share record (final signer message).
128+
*
129+
* There is no corresponding `verifyBitGoMessageRoundThree` because Wallet Platform
130+
* finalises the signing server-side after receiving round 3; the client obtains the
131+
* signed transaction via `sendTxRequest`.
132+
*/
133+
export async function getSignatureShareRoundThree(
134+
userMsg3: RedPallasMPSTypes.DeserializedMessage,
135+
userGpgPrivKey: openpgp.PrivateKey,
136+
partyId: SignerPartyId = MPCv2PartiesEnum.USER,
137+
otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
138+
): Promise<SignatureShareRecord> {
139+
const signedMsg3 = await RedPallasMPSComms.detachSignMpsMessage(Buffer.from(userMsg3.payload), userGpgPrivKey);
140+
const share: RedpallasMPCv2SignatureShareRound3Input = {
141+
type: 'round3Input',
142+
data: { msg3: signedMsg3 },
143+
};
144+
return {
145+
from: partyIdToSignatureShareType(partyId),
146+
to: partyIdToSignatureShareType(otherSignerPartyId),
147+
share: JSON.stringify(share),
148+
};
149+
}

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { decodeWithCodec } from '../utils/codecs';
5656
import { postWithCodec } from '../utils/postWithCodec';
5757
import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa';
5858
import EddsaUtils, { EddsaMPCv2Utils } from '../utils/tss/eddsa';
59+
import { RedpallasMPCv2Utils } from '../utils/tss/redpallas';
5960
import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest';
6061
import { buildParamKeys, BuildParams } from './BuildParams';
6162
import {
@@ -239,7 +240,13 @@ export class Wallet implements IWallet {
239240
public readonly baseCoin: IBaseCoin;
240241
public _wallet: WalletData;
241242
private _defi?: DefiVault;
242-
private readonly tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | undefined;
243+
private readonly tssUtils:
244+
| EcdsaUtils
245+
| EcdsaMPCv2Utils
246+
| EddsaUtils
247+
| EddsaMPCv2Utils
248+
| RedpallasMPCv2Utils
249+
| undefined;
243250
private readonly _permissions?: string[];
244251
/** Root keychain from passphrase preflight; consumed by getUserPrv to avoid a second GET. */
245252
private validatedSafeRootKeychain?: KeychainWithEncryptedPrv;
@@ -269,6 +276,10 @@ export class Wallet implements IWallet {
269276
this.tssUtils = new EddsaUtils(bitgo, baseCoin, this);
270277
}
271278
break;
279+
case 'redpallas':
280+
// RedPallas (Zcash Orchard shielded pool) is MPCv2-only; there is no MPCv1 variant.
281+
this.tssUtils = new RedpallasMPCv2Utils(bitgo, baseCoin, this);
282+
break;
272283
default:
273284
this.tssUtils = undefined;
274285
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import { sendSignatureShareV2 } from '../../../../src/bitgo/tss/common';
4+
import { BitGoBase, RequestType, TxRequest, MPCAlgorithm } from '../../../../src';
5+
6+
describe('sendSignatureShareV2 request type dispatch', function () {
7+
afterEach(function () {
8+
sinon.restore();
9+
});
10+
11+
async function captureRequestType(
12+
mpcAlgorithm: MPCAlgorithm,
13+
multisigTypeVersion: 'MPCv2' | undefined
14+
): Promise<string> {
15+
let capturedBody: { type: string } | undefined;
16+
const send = sinon.stub().callsFake((body) => {
17+
capturedBody = body;
18+
return { result: sinon.stub().resolves({} as TxRequest) };
19+
});
20+
const mockBitGo = {
21+
url: sinon.stub().returns('/mock/url'),
22+
post: sinon.stub().returns({ send }),
23+
setRequestTracer: sinon.stub(),
24+
} as unknown as BitGoBase;
25+
26+
await sendSignatureShareV2(
27+
mockBitGo,
28+
'walletId',
29+
'txRequestId',
30+
[],
31+
RequestType.tx,
32+
mpcAlgorithm,
33+
'signerGpgPublicKey',
34+
undefined,
35+
multisigTypeVersion
36+
);
37+
38+
if (!capturedBody) {
39+
throw new Error('request body should have been captured');
40+
}
41+
return capturedBody.type;
42+
}
43+
44+
it('resolves ecdsaMpcV2 for MPCv2 + ecdsa', async function () {
45+
assert.strictEqual(await captureRequestType('ecdsa', 'MPCv2'), 'ecdsaMpcV2');
46+
});
47+
48+
it('resolves eddsaMpcV2 for MPCv2 + eddsa', async function () {
49+
assert.strictEqual(await captureRequestType('eddsa', 'MPCv2'), 'eddsaMpcV2');
50+
});
51+
52+
it('resolves redpallasMpcV2 for MPCv2 + redpallas', async function () {
53+
assert.strictEqual(await captureRequestType('redpallas', 'MPCv2'), 'redpallasMpcV2');
54+
});
55+
56+
it('resolves eddsaMpcV1 for undefined multisigTypeVersion + eddsa', async function () {
57+
assert.strictEqual(await captureRequestType('eddsa', undefined), 'eddsaMpcV1');
58+
});
59+
60+
it('resolves an empty type for redpallas without MPCv2 (no MPCv1 variant exists)', async function () {
61+
assert.strictEqual(await captureRequestType('redpallas', undefined), '');
62+
});
63+
});

0 commit comments

Comments
 (0)