Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IPendingApprovals } from '../pendingApproval';
import { InitiateRecoveryOptions } from '../recovery';
import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa';
import EddsaUtils, { EddsaMPCv2Utils, PrebuildTransactionWithIntentOptions, TxRequest } from '../utils/tss/eddsa';
import { RedpallasMPCv2Utils } from '../utils/tss/redpallas';
import { CreateAddressFormat, CustomSigningFunction, IWallet, IWallets, Memo, Wallet, WalletData } from '../wallet';

import { TokenEnablement } from '@bitgo/public-types';
Expand Down Expand Up @@ -364,7 +365,7 @@ export interface ExtraPrebuildParamsOptions {
export interface PresignTransactionOptions {
txPrebuild?: TransactionPrebuild;
walletData: WalletData;
tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | undefined;
tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | RedpallasMPCv2Utils | undefined;
[index: string]: unknown;
}

Expand Down Expand Up @@ -628,11 +629,14 @@ export interface MessagePrep {
}

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

Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-core/src/bitgo/tss/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export async function sendSignatureShareV2(
type = 'ecdsaMpcV2';
} else if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'eddsa') {
type = 'eddsaMpcV2';
} else if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'redpallas') {
type = 'redpallasMpcV2';
} else if (multisigTypeVersion === undefined && mpcAlgorithm === 'eddsa') {
type = 'eddsaMpcV1';
}
Expand Down
149 changes: 149 additions & 0 deletions modules/sdk-core/src/bitgo/tss/redpallas/redpallasMPCv2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as openpgp from 'openpgp';
import { RedPallasMPSComms, RedPallasMPSTypes } from '@bitgo/sdk-lib-mpc';
import {
RedpallasMPCv2SignatureShareRound1Input,
RedpallasMPCv2SignatureShareRound1Output,
RedpallasMPCv2SignatureShareRound2Input,
RedpallasMPCv2SignatureShareRound2Output,
RedpallasMPCv2SignatureShareRound3Input,
RedpallasMPCv2SignatureShareRound3Output,
} from '@bitgo/public-types';
import { SignatureShareRecord, SignatureShareType } from '../../utils/tss/baseTypes';
import { MPCv2PartiesEnum } from '../../utils/tss/ecdsa/typesMPCv2';

type SignerPartyId = MPCv2PartiesEnum.USER | MPCv2PartiesEnum.BACKUP;

function partyIdToSignatureShareType(partyId: MPCv2PartiesEnum): SignatureShareType {
switch (partyId) {
case MPCv2PartiesEnum.USER:
return SignatureShareType.USER;
case MPCv2PartiesEnum.BACKUP:
return SignatureShareType.BACKUP;
case MPCv2PartiesEnum.BITGO:
return SignatureShareType.BITGO;
}
}

/**
* RedPallas MPS DSG signature-share helpers.
*
* Groundwork for a future custodial/cold (SMC/OVC) DSG signing flow - not yet wired up to any
* caller in this SDK. Mirrors `../eddsa/eddsaMPCv2.ts` (same 3-round shape, same PGP-signed-
* message envelope), but kept as an independent copy - built on `RedPallasMPSComms` - so
* RedPallas MPS never depends on the EdDSA MPS module, and vice versa.
*/

/**
* Builds the round-1 signature share record.
*
* PGP-signs the WASM round-0 broadcast message with the signer's ephemeral key and
* wraps it into a SignatureShareRecord ready for `sendSignatureShareV2`.
*/
export async function getSignatureShareRoundOne(
userMsg1: RedPallasMPSTypes.DeserializedMessage,
userGpgPrivKey: openpgp.PrivateKey,
partyId: SignerPartyId = MPCv2PartiesEnum.USER,
otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
): Promise<SignatureShareRecord> {
const signedMsg1 = await RedPallasMPSComms.detachSignMpsMessage(Buffer.from(userMsg1.payload), userGpgPrivKey);
const share: RedpallasMPCv2SignatureShareRound1Input = {
type: 'round1Input',
data: { msg1: signedMsg1 },
};
return {
from: partyIdToSignatureShareType(partyId),
to: partyIdToSignatureShareType(otherSignerPartyId),
share: JSON.stringify(share),
};
}

/**
* Verifies the peer's round-1 PGP signature and returns the raw deserialized
* message ready for `RedPallasDSG.handleIncomingMessages`.
*/
export async function verifyPeerMessageRoundOne(
parsedRound1Output: RedpallasMPCv2SignatureShareRound1Output,
peerGpgKey: openpgp.Key,
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
): Promise<RedPallasMPSTypes.DeserializedMessage> {
const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsedRound1Output.data.msg1, peerGpgKey);
return {
from: peerPartyId,
payload: new Uint8Array(rawBytes),
};
}

/**
* Builds the round-2 signature share record.
*/
export async function getSignatureShareRoundTwo(
userMsg2: RedPallasMPSTypes.DeserializedMessage,
userGpgPrivKey: openpgp.PrivateKey,
partyId: SignerPartyId = MPCv2PartiesEnum.USER,
otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
): Promise<SignatureShareRecord> {
const signedMsg2 = await RedPallasMPSComms.detachSignMpsMessage(Buffer.from(userMsg2.payload), userGpgPrivKey);
const share: RedpallasMPCv2SignatureShareRound2Input = {
type: 'round2Input',
data: { msg2: signedMsg2 },
};
return {
from: partyIdToSignatureShareType(partyId),
to: partyIdToSignatureShareType(otherSignerPartyId),
share: JSON.stringify(share),
};
}

/**
* Verifies the peer's round-2 PGP signature and returns the raw deserialized
* message ready for `RedPallasDSG.handleIncomingMessages`.
*/
export async function verifyPeerMessageRoundTwo(
parsedRound2Output: RedpallasMPCv2SignatureShareRound2Output,
peerGpgKey: openpgp.Key,
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
): Promise<RedPallasMPSTypes.DeserializedMessage> {
const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsedRound2Output.data.msg2, peerGpgKey);
return {
from: peerPartyId,
payload: new Uint8Array(rawBytes),
};
}

/**
* Verifies the peer's round-3 PGP signature and returns the raw deserialized
* message ready for `RedPallasDSG.handleIncomingMessages`.
*/
export async function verifyPeerMessageRoundThree(
parsedRound3Output: RedpallasMPCv2SignatureShareRound3Output,
peerGpgKey: openpgp.Key,
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
): Promise<RedPallasMPSTypes.DeserializedMessage> {
const rawBytes = await RedPallasMPSComms.verifyMpsMessage(parsedRound3Output.data.msg3, peerGpgKey);
return { from: peerPartyId, payload: new Uint8Array(rawBytes) };
}

/**
* Builds the round-3 signature share record (final signer message).
*
* There is no corresponding `verifyBitGoMessageRoundThree` because Wallet Platform
* finalises the signing server-side after receiving round 3; the client obtains the
* signed transaction via `sendTxRequest`.
*/
export async function getSignatureShareRoundThree(
userMsg3: RedPallasMPSTypes.DeserializedMessage,
userGpgPrivKey: openpgp.PrivateKey,
partyId: SignerPartyId = MPCv2PartiesEnum.USER,
otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
): Promise<SignatureShareRecord> {
const signedMsg3 = await RedPallasMPSComms.detachSignMpsMessage(Buffer.from(userMsg3.payload), userGpgPrivKey);
const share: RedpallasMPCv2SignatureShareRound3Input = {
type: 'round3Input',
data: { msg3: signedMsg3 },
};
return {
from: partyIdToSignatureShareType(partyId),
to: partyIdToSignatureShareType(otherSignerPartyId),
share: JSON.stringify(share),
};
}
13 changes: 12 additions & 1 deletion modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { decodeWithCodec } from '../utils/codecs';
import { postWithCodec } from '../utils/postWithCodec';
import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa';
import EddsaUtils, { EddsaMPCv2Utils } from '../utils/tss/eddsa';
import { RedpallasMPCv2Utils } from '../utils/tss/redpallas';
import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest';
import { buildParamKeys, BuildParams } from './BuildParams';
import {
Expand Down Expand Up @@ -239,7 +240,13 @@ export class Wallet implements IWallet {
public readonly baseCoin: IBaseCoin;
public _wallet: WalletData;
private _defi?: DefiVault;
private readonly tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | undefined;
private readonly tssUtils:
| EcdsaUtils
| EcdsaMPCv2Utils
| EddsaUtils
| EddsaMPCv2Utils
| RedpallasMPCv2Utils
| undefined;
private readonly _permissions?: string[];
/** Root keychain from passphrase preflight; consumed by getUserPrv to avoid a second GET. */
private validatedSafeRootKeychain?: KeychainWithEncryptedPrv;
Expand Down Expand Up @@ -269,6 +276,10 @@ export class Wallet implements IWallet {
this.tssUtils = new EddsaUtils(bitgo, baseCoin, this);
}
break;
case 'redpallas':
// RedPallas (Zcash Orchard shielded pool) is MPCv2-only; there is no MPCv1 variant.
this.tssUtils = new RedpallasMPCv2Utils(bitgo, baseCoin, this);
break;
default:
this.tssUtils = undefined;
}
Expand Down
63 changes: 63 additions & 0 deletions modules/sdk-core/test/unit/bitgo/tss/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import { sendSignatureShareV2 } from '../../../../src/bitgo/tss/common';
import { BitGoBase, RequestType, TxRequest, MPCAlgorithm } from '../../../../src';

describe('sendSignatureShareV2 request type dispatch', function () {
afterEach(function () {
sinon.restore();
});

async function captureRequestType(
mpcAlgorithm: MPCAlgorithm,
multisigTypeVersion: 'MPCv2' | undefined
): Promise<string> {
let capturedBody: { type: string } | undefined;
const send = sinon.stub().callsFake((body) => {
capturedBody = body;
return { result: sinon.stub().resolves({} as TxRequest) };
});
const mockBitGo = {
url: sinon.stub().returns('/mock/url'),
post: sinon.stub().returns({ send }),
setRequestTracer: sinon.stub(),
} as unknown as BitGoBase;

await sendSignatureShareV2(
mockBitGo,
'walletId',
'txRequestId',
[],
RequestType.tx,
mpcAlgorithm,
'signerGpgPublicKey',
undefined,
multisigTypeVersion
);

if (!capturedBody) {
throw new Error('request body should have been captured');
}
return capturedBody.type;
}

it('resolves ecdsaMpcV2 for MPCv2 + ecdsa', async function () {
assert.strictEqual(await captureRequestType('ecdsa', 'MPCv2'), 'ecdsaMpcV2');
});

it('resolves eddsaMpcV2 for MPCv2 + eddsa', async function () {
assert.strictEqual(await captureRequestType('eddsa', 'MPCv2'), 'eddsaMpcV2');
});

it('resolves redpallasMpcV2 for MPCv2 + redpallas', async function () {
assert.strictEqual(await captureRequestType('redpallas', 'MPCv2'), 'redpallasMpcV2');
});

it('resolves eddsaMpcV1 for undefined multisigTypeVersion + eddsa', async function () {
assert.strictEqual(await captureRequestType('eddsa', undefined), 'eddsaMpcV1');
});

it('resolves an empty type for redpallas without MPCv2 (no MPCv1 variant exists)', async function () {
assert.strictEqual(await captureRequestType('redpallas', undefined), '');
});
});
Loading
Loading