Skip to content
Draft
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
2 changes: 1 addition & 1 deletion modules/abstract-lightning/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
]
},
"dependencies": {
"@bitgo/public-types": "6.58.0",
"@bitgo/public-types": "6.63.0",
"@bitgo/sdk-core": "^38.10.0",
"@bitgo/statics": "^59.9.0",
"@bitgo/utxo-lib": "^11.24.2",
Expand Down
2 changes: 1 addition & 1 deletion modules/bitgo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
"superagent": "^9.0.1"
},
"devDependencies": {
"@bitgo/public-types": "6.58.0",
"@bitgo/public-types": "6.63.0",
"@bitgo/sdk-opensslbytes": "^2.1.0",
"@bitgo/sdk-test": "^9.1.70",
"@openpgp/web-stream-tools": "0.0.14",
Expand Down
2 changes: 1 addition & 1 deletion modules/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"superagent": "^9.0.1"
},
"devDependencies": {
"@bitgo/public-types": "6.58.0",
"@bitgo/public-types": "6.63.0",
"@bitgo/sdk-lib-mpc": "^10.17.0",
"@bitgo/sdk-test": "^9.1.70",
"@types/argparse": "^1.0.36",
Expand Down
2 changes: 1 addition & 1 deletion modules/passkey-crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"access": "public"
},
"dependencies": {
"@bitgo/public-types": "6.58.0",
"@bitgo/public-types": "6.63.0",
"@bitgo/sdk-core": "^38.10.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-coin-flrp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"nock": "^13.3.1"
},
"dependencies": {
"@bitgo/public-types": "6.58.0",
"@bitgo/public-types": "6.63.0",
"@bitgo/sdk-core": "^38.10.0",
"@bitgo/secp256k1": "^1.11.0",
"@bitgo/statics": "^59.9.0",
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-coin-sol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
},
"dependencies": {
"@bitgo/logger": "^1.4.0",
"@bitgo/public-types": "6.58.0",
"@bitgo/public-types": "6.63.0",
"@bitgo/sdk-core": "^38.10.0",
"@bitgo/sdk-lib-mpc": "^10.17.0",
"@bitgo/statics": "^59.9.0",
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
]
},
"dependencies": {
"@bitgo/public-types": "6.58.0",
"@bitgo/public-types": "6.63.0",
"@bitgo/sdk-lib-mpc": "^10.17.0",
"@bitgo/secp256k1": "^1.11.0",
"@bitgo/sjcl": "^1.1.0",
Expand Down
9 changes: 8 additions & 1 deletion modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,14 @@ export interface MessagePrep {
encodeMessage(message: string): string;
}

export type MPCAlgorithm = 'ecdsa' | 'eddsa';
/**
* '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.
*/
export type MPCAlgorithm = 'ecdsa' | 'eddsa' | 'redpallas';

export type NFTTransferOptions = {
tokenContractAddress: string;
Expand Down
3 changes: 2 additions & 1 deletion modules/sdk-core/src/bitgo/tss/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import openpgp from 'openpgp';
import { MPCv2SigningState } from '@bitgo/public-types';
import { BitGoBase } from '../bitgoBase';
import { TxRequestChallengeResponse } from './types';
import { MPCAlgorithm } from '../baseCoin';
import {
RequestType,
TxRequest,
Expand Down Expand Up @@ -144,7 +145,7 @@ export async function sendSignatureShareV2(
txRequestId: string,
signatureShares: SignatureShareRecord[],
requestType: RequestType,
mpcAlgorithm: 'eddsa' | 'ecdsa',
mpcAlgorithm: MPCAlgorithm,
signerGpgPublicKey: string,
signerShare?: string,
multisigTypeVersion?: 'MPCv2' | undefined,
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-core/src/bitgo/utils/tss/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import TssUtils, { TssUtilsTypes, EddsaUtilsTypes } from './eddsa';
export * as ECDSAUtils from './ecdsa';
export * as EDDSAUtils from './eddsa';
export * as RedpallasUtils from './redpallas';

/** @deprecated use EDDSAUtils */
export { TssUtils };
Expand Down
183 changes: 183 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/redpallas/SMC/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import assert from 'assert';
import {
OVCIndexEnum,
RedpallasBitgoToOVC1Round1Response,
RedpallasBitgoToOVC1Round2Response,
RedpallasKeyCreationMPCv2StateEnum,
RedpallasMPCv2KeyGenRound1Response,
RedpallasMPCv2KeyGenRound2Response,
RedpallasOVC1ToBitgoRound1Payload,
RedpallasOVC2ToBitgoRound2Payload,
} from '@bitgo/public-types';
import { IBaseCoin } from '../../../../baseCoin';
import { BitGoBase } from '../../../../bitgoBase';
import { decodeOrElse, Keychain } from '../../../..';
import { RedpallasMPCv2Utils } from '../redpallasMPCv2';
import { RedpallasMPCv2KeyGenSendFn, RedpallasKeyGenSenderForEnterprise } from '../redpallasMPCv2KeyGenSender';

/**
* Custodial (SMC/OVC) DKG-only key generation for the Zcash Orchard shielded pool (RedPallas).
*
* Mirrors for both custodial and SMC wallet
* ceremonies driven by external OVC (offline vault console) clients: round1/round2 payloads are
* relayed from OVC1/OVC2 to BitGo and back. There is no round3 (no signing) - RedPallas MPS DKG
* completes key generation in 2 online rounds, same as EdDSA MPS DKG.
*/
export class RedpallasMPCv2SMCUtils {
private MPCv2Utils: RedpallasMPCv2Utils;

constructor(private bitgo: BitGoBase, private baseCoin: IBaseCoin) {
this.MPCv2Utils = new RedpallasMPCv2Utils(bitgo, baseCoin);
}

public async keyGenRound1(
enterprise: string,
payload: RedpallasOVC1ToBitgoRound1Payload
): Promise<RedpallasBitgoToOVC1Round1Response> {
return this.keyGenRound1BySender(RedpallasKeyGenSenderForEnterprise(this.bitgo, enterprise), payload);
}

public async keyGenRound2(
enterprise: string,
payload: RedpallasOVC2ToBitgoRound2Payload
): Promise<RedpallasBitgoToOVC1Round2Response> {
return this.keyGenRound2BySender(RedpallasKeyGenSenderForEnterprise(this.bitgo, enterprise), payload);
}

public async keyGenRound1BySender(
senderFn: RedpallasMPCv2KeyGenSendFn<RedpallasMPCv2KeyGenRound1Response>,
payload: RedpallasOVC1ToBitgoRound1Payload
): Promise<RedpallasBitgoToOVC1Round1Response> {
assert(
payload.state === RedpallasKeyCreationMPCv2StateEnum.WaitingForBitgoRound1Data,
`Invalid state for round 1, expected: ${RedpallasKeyCreationMPCv2StateEnum.WaitingForBitgoRound1Data}, got: ${payload.state}`
);
decodeOrElse(RedpallasOVC1ToBitgoRound1Payload.name, RedpallasOVC1ToBitgoRound1Payload, payload, (errors) => {
throw new Error(`error(s) parsing payload: ${errors}`);
});

const ovc1 = payload.ovc[OVCIndexEnum.ONE];
const ovc2 = payload.ovc[OVCIndexEnum.TWO];
const result = await this.MPCv2Utils.sendKeyGenerationRound1BySender(senderFn, {
userGpgPublicKey: ovc1.gpgPubKey,
backupGpgPublicKey: ovc2.gpgPubKey,
userMsg1: ovc1.ovcMsg1,
backupMsg1: ovc2.ovcMsg1,
});

const response = {
state: RedpallasKeyCreationMPCv2StateEnum.WaitingForOVC1Round2Data,
tssVersion: payload.tssVersion,
walletType: payload.walletType,
coin: payload.coin,
ovc: payload.ovc,
platform: {
sessionId: result.sessionId,
bitgoMsg1: result.bitgoMsg1,
},
};

return decodeOrElse(
RedpallasBitgoToOVC1Round1Response.name,
RedpallasBitgoToOVC1Round1Response,
response,
(errors) => {
throw new Error(`error(s) parsing response: ${errors}`);
}
);
}

public async keyGenRound2BySender(
senderFn: RedpallasMPCv2KeyGenSendFn<RedpallasMPCv2KeyGenRound2Response>,
payload: RedpallasOVC2ToBitgoRound2Payload
): Promise<RedpallasBitgoToOVC1Round2Response> {
assert(
payload.state === RedpallasKeyCreationMPCv2StateEnum.WaitingForBitgoRound2Data,
`Invalid state for round 2, expected: ${RedpallasKeyCreationMPCv2StateEnum.WaitingForBitgoRound2Data}, got: ${payload.state}`
);
decodeOrElse(RedpallasOVC2ToBitgoRound2Payload.name, RedpallasOVC2ToBitgoRound2Payload, payload, (errors) => {
throw new Error(`error(s) parsing payload: ${errors}`);
});

const ovc1 = payload.ovc[OVCIndexEnum.ONE];
const ovc2 = payload.ovc[OVCIndexEnum.TWO];
const sessionId = payload.platform.sessionId;
const result = await this.MPCv2Utils.sendKeyGenerationRound2BySender(senderFn, {
sessionId,
userMsg2: ovc1.ovcMsg2,
backupMsg2: ovc2.ovcMsg2,
derivationSeed: payload.derivationSeed,
});

assert.equal(sessionId, result.sessionId, 'Round 1 and round 2 session IDs do not match');

const keychains = this.baseCoin.keychains();
const bitgoKeychain = await keychains.add({
source: 'bitgo',
keyType: 'tss',
commonKeychain: result.commonPublicKeychain,
isMPCv2: true,
});

const response = {
state: RedpallasKeyCreationMPCv2StateEnum.WaitingForOVC1GenerateKey,
bitGoKeyId: bitgoKeychain.id,
tssVersion: payload.tssVersion,
walletType: payload.walletType,
coin: payload.coin,
ovc: payload.ovc,
derivationSeed: payload.derivationSeed,
platform: {
// sessionId/bitgoMsg1 carried over from payload.platform; safe because the assert
// above guarantees payload.platform.sessionId equals result.sessionId.
...payload.platform,
commonPublicKeychain: result.commonPublicKeychain,
bitgoMsg2: result.bitgoMsg2,
},
};

return decodeOrElse(
RedpallasBitgoToOVC1Round2Response.name,
RedpallasBitgoToOVC1Round2Response,
response,
(errors) => {
throw new Error(`error(s) parsing response: ${errors}`);
}
);
}

public async uploadClientKeys(
bitgoKeyId: string,
userCommonKeychain: string,
backupCommonKeychain: string
): Promise<{ userKeychain: Keychain; backupKeychain: Keychain; bitgoKeychain: Keychain }> {
assert(
userCommonKeychain === backupCommonKeychain,
'Common keychain mismatch between the user and backup keychains'
);

const keychains = this.baseCoin.keychains();
const bitgoKeychain = await keychains.get({ id: bitgoKeyId });
assert(bitgoKeychain, 'Keychain not found');
assert(bitgoKeychain.source === 'bitgo', 'The keychain is not a BitGo keychain');
assert(bitgoKeychain.type === 'tss', 'BitGo keychain is not a TSS keychain');
assert(bitgoKeychain.commonKeychain, 'BitGo keychain does not have a common keychain');
assert(bitgoKeychain.commonKeychain === userCommonKeychain, 'Common keychain mismatch between the OVCs and BitGo');

const userKeychainPromise = keychains.add({
source: 'user',
keyType: 'tss',
commonKeychain: userCommonKeychain,
isMPCv2: true,
});
const backupKeychainPromise = keychains.add({
source: 'backup',
keyType: 'tss',
commonKeychain: backupCommonKeychain,
isMPCv2: true,
});

const [userKeychain, backupKeychain] = await Promise.all([userKeychainPromise, backupKeychainPromise]);
return { userKeychain, backupKeychain, bitgoKeychain };
}
}
3 changes: 3 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/redpallas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './redpallasMPCv2';
export * from './redpallasMPCv2KeyGenSender';
export * from './SMC/utils';
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
MPCv2KeyGenStateEnum,
RedpallasMPCv2KeyGenRound1Request,
RedpallasMPCv2KeyGenRound1Response,
RedpallasMPCv2KeyGenRound2Request,
RedpallasMPCv2KeyGenRound2Response,
} from '@bitgo/public-types';
import { IBaseCoin } from '../../../baseCoin';
import { BitGoBase } from '../../../bitgoBase';
import { IWallet } from '../../../wallet';
import BaseTssUtils from '../baseTSSUtils';
import {
GenerateRedpallasMPCv2KeyRequestBody,
GenerateRedpallasMPCv2KeyRequestResponse,
RedpallasKeyGenSenderForEnterprise,
RedpallasMPCv2KeyGenSendFn,
} from './redpallasMPCv2KeyGenSender';

/**
* DKG-only MPCv2 utils for the Zcash Orchard shielded pool (RedPallas / "Ironwood").
*
* This currently only exposes the BitGo-platform round1/round2 dispatch used by the custodial
* SMC/OVC ceremony (see `RedpallasMPCv2SMCUtils` in `./SMC/utils.ts`) - there is no self-custody
* (direct, non-OVC) key-generation entrypoint here, since nothing in this SDK constructs one
* today. There is also no signing (DSG) support - transaction signing for Zcash shielded
* addresses is out of scope for this SDK.
*
* RedPallas MPS DKG completes in the same 2-round shape as EdDSA MPS DKG (round0 local, round1 +
* round2 online), but round2 additionally requires a `derivationSeed`: a 32-byte value consumed by
* a subsequent, platform-side-only key derivation step (Zcash Orchard ask/nk/rivk/ivks) that is
* intentionally not implemented here. The resulting `commonPublicKeychain` is the raw 32-byte
* RedPallas group public key (64 hex chars) - there is no BIP32-style chain code, unlike EdDSA/ECDSA
* commonKeychains.
*/
export class RedpallasMPCv2Utils extends BaseTssUtils<unknown> {
constructor(bitgo: BitGoBase, baseCoin: IBaseCoin, wallet?: IWallet) {
super(bitgo, baseCoin, wallet);
this.setBitgoGpgPubKey(bitgo);
}

// #region platform round1/round2 dispatch

async sendKeyGenerationRound1(
enterprise: string,
payload: RedpallasMPCv2KeyGenRound1Request,
safeId?: string
): Promise<RedpallasMPCv2KeyGenRound1Response> {
return this.sendKeyGenerationRound1BySender(
RedpallasKeyGenSenderForEnterprise(this.bitgo, enterprise, safeId),
payload
);
}

async sendKeyGenerationRound1BySender(
senderFn: RedpallasMPCv2KeyGenSendFn<GenerateRedpallasMPCv2KeyRequestResponse>,
payload: RedpallasMPCv2KeyGenRound1Request
): Promise<RedpallasMPCv2KeyGenRound1Response> {
return senderFn(
MPCv2KeyGenStateEnum['MPCv2-R1'],
payload as GenerateRedpallasMPCv2KeyRequestBody
) as Promise<RedpallasMPCv2KeyGenRound1Response>;
}

async sendKeyGenerationRound2(
enterprise: string,
payload: RedpallasMPCv2KeyGenRound2Request
): Promise<RedpallasMPCv2KeyGenRound2Response> {
return this.sendKeyGenerationRound2BySender(RedpallasKeyGenSenderForEnterprise(this.bitgo, enterprise), payload);
}

async sendKeyGenerationRound2BySender(
senderFn: RedpallasMPCv2KeyGenSendFn<GenerateRedpallasMPCv2KeyRequestResponse>,
payload: RedpallasMPCv2KeyGenRound2Request
): Promise<RedpallasMPCv2KeyGenRound2Response> {
return senderFn(
MPCv2KeyGenStateEnum['MPCv2-R2'],
payload as GenerateRedpallasMPCv2KeyRequestBody
) as Promise<RedpallasMPCv2KeyGenRound2Response>;
}

// #endregion
}
Loading