Skip to content

Commit 3c7d9d2

Browse files
Merge pull request #5711 from BitGo/WIN-4344
feat(abstract-eth): added support for mpcv2 in recovery
2 parents 0827acd + da92bf8 commit 3c7d9d2

File tree

9 files changed

+568
-60
lines changed

9 files changed

+568
-60
lines changed

modules/abstract-eth/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
},
4242
"dependencies": {
4343
"@bitgo/sdk-core": "^32.0.1",
44+
"@bitgo/sdk-lib-mpc": "^10.1.2",
4445
"@bitgo/secp256k1": "^1.3.3",
4546
"@bitgo/statics": "^51.6.1",
4647
"@ethereumjs/common": "^2.6.5",

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 254 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
*/
44
import {
55
AddressCoinSpecific,
6+
PresignTransactionOptions as BasePresignTransactionOptions,
7+
SignTransactionOptions as BaseSignTransactionOptions,
8+
TransactionPrebuild as BaseTransactionPrebuild,
9+
VerifyAddressOptions as BaseVerifyAddressOptions,
610
BitGoBase,
711
BuildNftTransferDataOptions,
812
common,
913
Ecdsa,
1014
ECDSAMethodTypes,
15+
ECDSAUtils,
1116
EthereumLibraryUnavailableError,
1217
FeeEstimateOptions,
1318
FullySignedTransaction,
@@ -17,31 +22,31 @@ import {
1722
InvalidAddressVerificationObjectPropertyError,
1823
IWallet,
1924
KeyPair,
25+
MPCSweepRecoveryOptions,
26+
MPCTx,
27+
MPCTxs,
2028
ParsedTransaction,
2129
ParseTransactionOptions,
2230
PrebuildTransactionResult,
23-
PresignTransactionOptions as BasePresignTransactionOptions,
2431
Recipient,
25-
SignTransactionOptions as BaseSignTransactionOptions,
2632
TransactionParams,
27-
TransactionPrebuild as BaseTransactionPrebuild,
2833
TransactionRecipient,
2934
TypedData,
3035
UnexpectedAddressError,
36+
UnsignedTransactionTss,
3137
Util,
32-
VerifyAddressOptions as BaseVerifyAddressOptions,
3338
VerifyTransactionOptions,
3439
Wallet,
35-
ECDSAUtils,
3640
} from '@bitgo/sdk-core';
41+
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
42+
import { bip32 } from '@bitgo/secp256k1';
3743
import {
38-
BaseCoin as StaticsBaseCoin,
3944
CoinMap,
4045
coins,
41-
EthereumNetwork as EthLikeNetwork,
4246
ethGasConfigs,
47+
EthereumNetwork as EthLikeNetwork,
48+
BaseCoin as StaticsBaseCoin,
4349
} from '@bitgo/statics';
44-
import { bip32 } from '@bitgo/secp256k1';
4550
import type * as EthLikeCommon from '@ethereumjs/common';
4651
import type * as EthLikeTxLib from '@ethereumjs/tx';
4752
import { FeeMarketEIP1559Transaction, Transaction as LegacyTransaction } from '@ethereumjs/tx';
@@ -202,6 +207,19 @@ interface UnformattedTxInfo {
202207
recipient: Recipient;
203208
}
204209

210+
export type UnsignedSweepTxMPCv2 = {
211+
txRequests: {
212+
transactions: [
213+
{
214+
unsignedTx: UnsignedTransactionTss;
215+
nonce: number;
216+
signatureShares: [];
217+
}
218+
];
219+
walletCoin: string;
220+
}[];
221+
};
222+
205223
export type RecoverOptionsWithBytes = {
206224
isTss: true;
207225
/**
@@ -232,6 +250,7 @@ export type RecoverOptions = {
232250
tokenContractAddress?: string;
233251
intendedChain?: string;
234252
common?: EthLikeCommon.default;
253+
derivationSeed?: string;
235254
} & TSSRecoverOptions;
236255

237256
export type GetBatchExecutionInfoRT = {
@@ -1127,7 +1146,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
11271146
* @param {string} params.bitgoFeeAddress - wrong chain wallet fee address for evm based cross chain recovery txn
11281147
* @param {string} params.bitgoDestinationAddress - target bitgo address where fee will be sent for evm based cross chain recovery txn
11291148
*/
1130-
async recover(params: RecoverOptions): Promise<RecoveryInfo | OfflineVaultTxInfo> {
1149+
async recover(params: RecoverOptions): Promise<RecoveryInfo | OfflineVaultTxInfo | UnsignedSweepTxMPCv2> {
11311150
if (params.isTss === true) {
11321151
return this.recoverTSS(params);
11331152
}
@@ -1868,45 +1887,31 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
18681887
* Recovers a tx with TSS key shares
18691888
* same expected arguments as recover method, but with TSS key shares
18701889
*/
1871-
protected async recoverTSS(params: RecoverOptions): Promise<RecoveryInfo | OfflineVaultTxInfo> {
1890+
protected async recoverTSS(
1891+
params: RecoverOptions
1892+
): Promise<RecoveryInfo | OfflineVaultTxInfo | UnsignedSweepTxMPCv2> {
18721893
this.validateRecoveryParams(params);
18731894
// Clean up whitespace from entered values
18741895
const userPublicOrPrivateKeyShare = params.userKey.replace(/\s/g, '');
18751896
const backupPrivateOrPublicKeyShare = params.backupKey.replace(/\s/g, '');
18761897

1877-
const gasLimit = new optionalDeps.ethUtil.BN(this.setGasLimit(params.gasLimit));
1878-
const gasPrice = params.eip1559
1879-
? new optionalDeps.ethUtil.BN(params.eip1559.maxFeePerGas)
1880-
: new optionalDeps.ethUtil.BN(this.setGasPrice(params.gasPrice));
1881-
18821898
if (
18831899
getIsUnsignedSweep({
18841900
userKey: userPublicOrPrivateKeyShare,
18851901
backupKey: backupPrivateOrPublicKeyShare,
18861902
isTss: params.isTss,
18871903
})
18881904
) {
1889-
const backupKeyPair = new KeyPairLib({ pub: backupPrivateOrPublicKeyShare });
1890-
const baseAddress = backupKeyPair.getAddress();
1891-
const { txInfo, tx, nonce } = await this.buildTssRecoveryTxn(baseAddress, gasPrice, gasLimit, params);
1892-
return this.formatForOfflineVaultTSS(
1893-
txInfo,
1894-
tx,
1895-
userPublicOrPrivateKeyShare,
1896-
backupPrivateOrPublicKeyShare,
1897-
gasPrice,
1898-
gasLimit,
1899-
nonce,
1900-
params.eip1559,
1901-
params.replayProtectionOptions
1902-
);
1905+
return this.buildUnsignedSweepTxnTSS(params);
19031906
} else {
19041907
const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares(
19051908
userPublicOrPrivateKeyShare,
19061909
backupPrivateOrPublicKeyShare,
19071910
params.walletPassphrase
19081911
);
19091912

1913+
const { gasLimit, gasPrice } = await this.getGasValues(params);
1914+
19101915
const MPC = new Ecdsa();
19111916
const derivedCommonKeyChain = MPC.deriveUnhardened(commonKeyChain, 'm/0');
19121917
const backupKeyPair = new KeyPairLib({ pub: derivedCommonKeyChain.slice(0, 66) });
@@ -1924,6 +1929,226 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
19241929
}
19251930
}
19261931

1932+
private async getGasValues(params: RecoverOptions): Promise<{ gasLimit: number; gasPrice: Buffer }> {
1933+
const gasLimit = new optionalDeps.ethUtil.BN(this.setGasLimit(params.gasLimit));
1934+
const gasPrice = params.eip1559
1935+
? new optionalDeps.ethUtil.BN(params.eip1559.maxFeePerGas)
1936+
: new optionalDeps.ethUtil.BN(this.setGasPrice(params.gasPrice));
1937+
return { gasLimit, gasPrice };
1938+
}
1939+
1940+
protected async buildUnsignedSweepTxnTSS(params: RecoverOptions): Promise<OfflineVaultTxInfo | UnsignedSweepTxMPCv2> {
1941+
const userPublicOrPrivateKeyShare = params.userKey.replace(/\s/g, '');
1942+
const backupPrivateOrPublicKeyShare = params.backupKey.replace(/\s/g, '');
1943+
1944+
const { gasLimit, gasPrice } = await this.getGasValues(params);
1945+
1946+
const backupKeyPair = new KeyPairLib({ pub: backupPrivateOrPublicKeyShare });
1947+
const baseAddress = backupKeyPair.getAddress();
1948+
const { txInfo, tx, nonce } = await this.buildTssRecoveryTxn(baseAddress, gasPrice, gasLimit, params);
1949+
return this.formatForOfflineVaultTSS(
1950+
txInfo,
1951+
tx,
1952+
userPublicOrPrivateKeyShare,
1953+
backupPrivateOrPublicKeyShare,
1954+
gasPrice,
1955+
gasLimit,
1956+
nonce,
1957+
params.eip1559,
1958+
params.replayProtectionOptions
1959+
);
1960+
}
1961+
1962+
protected async buildUnsignedSweepTxnMPCv2(params: RecoverOptions): Promise<UnsignedSweepTxMPCv2> {
1963+
const { gasLimit, gasPrice } = await this.getGasValues(params);
1964+
1965+
const recoverParams = params as RecoverOptions;
1966+
this.validateUnsignedSweepTSSParams(recoverParams);
1967+
1968+
const derivationPath = recoverParams.derivationSeed ? getDerivationPath(recoverParams.derivationSeed) : 'm/0';
1969+
const MPC = new Ecdsa();
1970+
const derivedCommonKeyChain = MPC.deriveUnhardened(recoverParams.backupKey as string, derivationPath);
1971+
const backupKeyPair = new KeyPairLib({ pub: derivedCommonKeyChain.slice(0, 66) });
1972+
const baseAddress = backupKeyPair.getAddress();
1973+
const { txInfo, tx, nonce } = await this.buildTssRecoveryTxn(baseAddress, gasPrice, gasLimit, params);
1974+
return this.buildTxRequestForOfflineVaultMPCv2(
1975+
txInfo,
1976+
tx,
1977+
derivationPath,
1978+
nonce,
1979+
gasPrice,
1980+
gasLimit,
1981+
params.eip1559,
1982+
params.replayProtectionOptions,
1983+
recoverParams.backupKey as string
1984+
);
1985+
}
1986+
1987+
async createBroadcastableSweepTransaction(params: MPCSweepRecoveryOptions): Promise<MPCTxs> {
1988+
const req = params.signatureShares;
1989+
const broadcastableTransactions: MPCTx[] = [];
1990+
let lastScanIndex = 0;
1991+
1992+
for (let i = 0; i < req.length; i++) {
1993+
const MPC = new Ecdsa();
1994+
const transaction = req[i]?.txRequest?.transactions?.[0]?.unsignedTx as unknown as UnsignedTransactionTss;
1995+
if (!req[i].ovc || !req[i].ovc[0].ecdsaSignature) {
1996+
throw new Error('Missing signature(s)');
1997+
}
1998+
if (!transaction.signableHex) {
1999+
throw new Error('Missing signable hex');
2000+
}
2001+
const signature = req[i].ovc[0].ecdsaSignature;
2002+
if (!signature) {
2003+
throw new Error('Signature is undefined');
2004+
}
2005+
const shares: string[] = signature.toString().split(':');
2006+
if (shares.length !== 4) {
2007+
throw new Error('Invalid signature');
2008+
}
2009+
const finalSignature: ECDSAMethodTypes.Signature = {
2010+
recid: Number(shares[0]),
2011+
r: shares[1],
2012+
s: shares[2],
2013+
y: shares[3],
2014+
} as unknown as ECDSAMethodTypes.Signature;
2015+
const signatureHex = Buffer.from(signature.toString(), 'hex');
2016+
const txBuilder = this.getTransactionBuilder(getCommon(this.getNetwork() as EthLikeNetwork));
2017+
txBuilder.from(transaction.serializedTxHex as string);
2018+
2019+
if (!transaction.coinSpecific?.commonKeyChain) {
2020+
throw new Error(`Missing common keychain for transaction at index ${i}`);
2021+
}
2022+
const commonKeyChain = transaction.coinSpecific.commonKeyChain;
2023+
if (!transaction.derivationPath) {
2024+
throw new Error(`Missing derivation path for transaction at index ${i}`);
2025+
}
2026+
if (!commonKeyChain) {
2027+
throw new Error(`Missing common key chain for transaction at index ${i}`);
2028+
}
2029+
2030+
const derivationPath = transaction.derivationPath ?? 'm/0';
2031+
const derivedCommonKeyChain = MPC.deriveUnhardened(String(commonKeyChain), String(derivationPath));
2032+
const derivedPublicKey = new KeyPairLib({ pub: derivedCommonKeyChain.slice(0, 66) });
2033+
txBuilder.addSignature({ pub: derivedPublicKey.getKeys().pub }, signatureHex);
2034+
const ethCommmon = AbstractEthLikeNewCoins.getEthLikeCommon(
2035+
transaction.eip1559,
2036+
transaction.replayProtectionOptions
2037+
);
2038+
let unsignedTx;
2039+
if (transaction.eip1559) {
2040+
unsignedTx = await FeeMarketEIP1559Transaction.fromSerializedTx(
2041+
Buffer.from(transaction.serializedTxHex, 'hex')
2042+
);
2043+
} else {
2044+
unsignedTx = await LegacyTransaction.fromSerializedTx(Buffer.from(transaction.serializedTxHex, 'hex'));
2045+
}
2046+
const signedTx = this.getSignedTxFromSignature(ethCommmon, unsignedTx, finalSignature);
2047+
broadcastableTransactions.push({
2048+
serializedTx: addHexPrefix(signedTx.serialize().toString('hex')),
2049+
});
2050+
2051+
if (i === req.length - 1 && transaction.coinSpecific!.lastScanIndex) {
2052+
lastScanIndex = transaction.coinSpecific!.lastScanIndex as number;
2053+
}
2054+
}
2055+
2056+
return { transactions: broadcastableTransactions, lastScanIndex };
2057+
}
2058+
2059+
/**
2060+
* Method to validate recovery params
2061+
* @param {RecoverOptions} params
2062+
* @returns {void}
2063+
*/
2064+
private async validateUnsignedSweepTSSParams(params: RecoverOptions): Promise<void> {
2065+
if (_.isUndefined(params.backupKey) && params.backupKey === '') {
2066+
throw new Error('missing commonKeyChain');
2067+
}
2068+
if (!_.isUndefined(params.derivationSeed) && typeof params.derivationSeed !== 'string') {
2069+
throw new Error('invalid derivationSeed');
2070+
}
2071+
if (
2072+
_.isUndefined(params.bitgoDestinationAddress) ||
2073+
typeof params.bitgoDestinationAddress !== 'string' ||
2074+
!this.isValidAddress(params.bitgoDestinationAddress)
2075+
) {
2076+
throw new Error('missing or invalid destinationAddress');
2077+
}
2078+
}
2079+
2080+
/**
2081+
* Helper function for recover()
2082+
* This transforms the unsigned transaction information into a format the BitGo offline vault expects
2083+
* @param {UnformattedTxInfo} txInfo - tx info
2084+
* @param {EthLikeTxLib.Transaction | EthLikeTxLib.FeeMarketEIP1559Transaction} ethTx - the ethereumjs tx object
2085+
* @param {string} derivationPath - the derivationPath
2086+
* @param {number} nonce - the nonce of the backup key address
2087+
* @param {Buffer} gasPrice - gas price for the tx
2088+
* @param {number} gasLimit - gas limit for the tx
2089+
* @param {EIP1559} eip1559 - eip1559 params
2090+
* @returns {Promise<OfflineVaultTxInfo>}
2091+
*/
2092+
private buildTxRequestForOfflineVaultMPCv2(
2093+
txInfo: UnformattedTxInfo,
2094+
ethTx: EthLikeTxLib.Transaction | EthLikeTxLib.FeeMarketEIP1559Transaction,
2095+
derivationPath: string,
2096+
nonce: number,
2097+
gasPrice: Buffer,
2098+
gasLimit: number,
2099+
eip1559?: EIP1559,
2100+
replayProtectionOptions?: ReplayProtectionOptions,
2101+
commonKeyChain?: string
2102+
): UnsignedSweepTxMPCv2 {
2103+
if (!ethTx.to) {
2104+
throw new Error('Eth tx must have a `to` address');
2105+
}
2106+
2107+
const fee = eip1559
2108+
? gasLimit * eip1559.maxFeePerGas
2109+
: gasLimit * optionalDeps.ethUtil.bufferToInt(gasPrice).toFixed();
2110+
2111+
const unsignedTx: UnsignedTransactionTss = {
2112+
serializedTxHex: ethTx.serialize().toString('hex'),
2113+
signableHex: ethTx.getMessageToSign(false).toString('hex'),
2114+
derivationPath: derivationPath,
2115+
feeInfo: {
2116+
fee: fee,
2117+
feeString: fee.toString(),
2118+
},
2119+
parsedTx: {
2120+
spendAmount: txInfo.recipient.amount,
2121+
outputs: [
2122+
{
2123+
coinName: this.getChain(),
2124+
address: txInfo.recipient.address,
2125+
valueString: txInfo.recipient.amount,
2126+
},
2127+
],
2128+
},
2129+
coinSpecific: {
2130+
commonKeyChain: commonKeyChain,
2131+
},
2132+
eip1559: eip1559,
2133+
replayProtectionOptions: replayProtectionOptions,
2134+
};
2135+
2136+
return {
2137+
txRequests: [
2138+
{
2139+
walletCoin: this.getChain(),
2140+
transactions: [
2141+
{
2142+
unsignedTx: unsignedTx,
2143+
nonce: nonce,
2144+
signatureShares: [],
2145+
},
2146+
],
2147+
},
2148+
],
2149+
};
2150+
}
2151+
19272152
private async buildTssRecoveryTxn(baseAddress: string, gasPrice: any, gasLimit: any, params: RecoverOptions) {
19282153
const nonce = await this.getAddressNonce(baseAddress);
19292154
const txAmount = await this.validateBalanceAndGetTxAmount(baseAddress, gasPrice, gasLimit);
@@ -1957,7 +2182,6 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
19572182

19582183
async validateBalanceAndGetTxAmount(baseAddress: string, gasPrice: BN, gasLimit: BN) {
19592184
const baseAddressBalance = await this.queryAddressBalance(baseAddress);
1960-
19612185
const totalGasNeeded = gasPrice.mul(gasLimit);
19622186
const weiToGwei = new BN(10 ** 9);
19632187
if (baseAddressBalance.lt(totalGasNeeded)) {
@@ -1967,7 +2191,6 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
19672191
` Gwei to perform recoveries. Try sending some ETH to this address then retry.`
19682192
);
19692193
}
1970-
19712194
const txAmount = baseAddressBalance.sub(totalGasNeeded);
19722195
return txAmount;
19732196
}

0 commit comments

Comments
 (0)