Skip to content

Commit 0d70111

Browse files
Merge pull request #45 from BitGo/WP-5157
fix: missing params broadcast fail on musig eth rec
2 parents 2a5094f + a50db48 commit 0d70111

File tree

4 files changed

+183
-11
lines changed

4 files changed

+183
-11
lines changed

src/api/enclaved/handlers/recoveryMultisigTransaction.ts

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import { MethodNotImplementedError } from 'bitgo';
33
import { EnclavedApiSpecRouteRequest } from '../../../enclavedBitgoExpress/routers/enclavedApiSpec';
44
import logger from '../../../logger';
55
import { isEthLikeCoin } from '../../../shared/coinUtils';
6+
import {
7+
addEthLikeRecoveryExtras,
8+
getDefaultMusigEthGasParams,
9+
getReplayProtectionOptions,
10+
} from '../../../shared/recoveryUtils';
11+
import { SignedEthLikeRecoveryTx } from '../../../types/transaction';
612
import { retrieveKmsPrvKey } from '../utils';
7-
import { HalfSignedEthLikeRecoveryTx } from '../../../types/transaction';
813

914
export async function recoveryMultisigTransaction(
1015
req: EnclavedApiSpecRouteRequest<'v1.multisig.recovery', 'post'>,
@@ -29,28 +34,79 @@ export async function recoveryMultisigTransaction(
2934
if (coin.isEVM()) {
3035
// Every recovery method on every coin family varies one from another so we need to ensure with a guard.
3136
if (isEthLikeCoin(coin)) {
37+
const walletKeys = unsignedSweepPrebuildTx.xpubxWithDerivationPath;
38+
const pubs = [walletKeys?.user?.xpub, walletKeys?.backup?.xpub, walletKeys?.bitgo?.xpub];
39+
const { gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas } =
40+
getDefaultMusigEthGasParams();
41+
3242
try {
33-
const halfSignedTx = await coin.signTransaction({
43+
const halfSignedTxBase = await coin.signTransaction({
3444
isLastSignature: false,
3545
prv: userPrv,
36-
txPrebuild: { ...unsignedSweepPrebuildTx } as unknown as SignFinalOptions,
46+
pubs,
47+
keyList: walletKeys,
48+
recipients: unsignedSweepPrebuildTx.recipients ?? [],
49+
expireTime: unsignedSweepPrebuildTx.expireTime,
50+
signingKeyNonce: unsignedSweepPrebuildTx.signingKeyNonce,
51+
gasPrice,
52+
gasLimit,
53+
eip1559: {
54+
maxFeePerGas,
55+
maxPriorityFeePerGas,
56+
},
57+
replayProtectionOptions: getReplayProtectionOptions(
58+
unsignedSweepPrebuildTx.replayProtectionOptions,
59+
),
60+
txPrebuild: {
61+
...unsignedSweepPrebuildTx,
62+
gasPrice,
63+
gasLimit,
64+
eip1559: {
65+
maxFeePerGas,
66+
maxPriorityFeePerGas,
67+
},
68+
replayProtectionOptions: getReplayProtectionOptions(
69+
unsignedSweepPrebuildTx.replayProtectionOptions,
70+
),
71+
},
3772
walletContractAddress,
3873
});
3974

40-
const { halfSigned } = halfSignedTx as HalfSignedEthLikeRecoveryTx;
75+
const halfSignedTx = addEthLikeRecoveryExtras({
76+
signedTx: halfSignedTxBase as SignedEthLikeRecoveryTx,
77+
transaction: unsignedSweepPrebuildTx,
78+
isLastSignature: false,
79+
replayProtectionOptions: unsignedSweepPrebuildTx.replayProtectionOptions,
80+
});
81+
82+
const { halfSigned } = halfSignedTx;
4183
const fullSignedTx = await coin.signTransaction({
4284
isLastSignature: true,
4385
prv: backupPrv,
86+
pubs,
87+
keyList: walletKeys,
88+
recipients: halfSignedTx.recipients ?? [],
89+
expireTime: halfSigned?.expireTime,
90+
signingKeyNonce: halfSigned?.backupKeyNonce,
91+
gasPrice,
92+
gasLimit,
4493
txPrebuild: {
4594
...halfSignedTx,
46-
txHex: halfSigned.signatures,
95+
txHex: halfSigned?.txHex,
4796
halfSigned,
48-
recipients: halfSigned.recipients ?? [],
97+
recipients: halfSigned?.recipients ?? [],
98+
gasPrice,
99+
gasLimit,
100+
eip1559: {
101+
maxFeePerGas,
102+
maxPriorityFeePerGas,
103+
},
104+
replayProtectionOptions: getReplayProtectionOptions(
105+
halfSignedTx?.replayProtectionOptions,
106+
),
49107
} as unknown as SignFinalOptions,
50108
walletContractAddress,
51-
signingKeyNonce: halfSigned.signingKeyNonce ?? 0,
52-
backupKeyNonce: halfSigned.backupKeyNonce ?? 0,
53-
recipients: halfSigned.recipients ?? [],
109+
backupKeyNonce: halfSigned?.backupKeyNonce ?? 0,
54110
});
55111

56112
return fullSignedTx;

src/api/master/handlers/recoveryWallet.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { MethodNotImplementedError } from 'bitgo';
22
import { isEthLikeCoin } from '../../../shared/coinUtils';
3+
import {
4+
getDefaultMusigEthGasParams,
5+
getReplayProtectionOptions,
6+
} from '../../../shared/recoveryUtils';
37
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
48

59
export async function handleRecoveryWalletOnPrem(
@@ -31,9 +35,19 @@ export async function handleRecoveryWalletOnPrem(
3135

3236
if (isEthLikeCoin(sdkCoin)) {
3337
try {
38+
const { gasLimit, gasPrice, maxFeePerGas, maxPriorityFeePerGas } =
39+
getDefaultMusigEthGasParams();
3440
const unsignedSweepPrebuildTx = await sdkCoin.recover({
3541
...commonRecoveryParams,
42+
gasPrice,
43+
gasLimit,
44+
eip1559: {
45+
maxFeePerGas,
46+
maxPriorityFeePerGas,
47+
},
48+
replayProtectionOptions: getReplayProtectionOptions(),
3649
});
50+
3751
const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({
3852
userPub,
3953
backupPub,

src/shared/recoveryUtils.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { ReplayProtectionOptions, SignedEthLikeRecoveryTx } from '../types/transaction';
2+
3+
export function addEthLikeRecoveryExtras({
4+
signedTx,
5+
transaction,
6+
isLastSignature,
7+
replayProtectionOptions,
8+
}: {
9+
signedTx: SignedEthLikeRecoveryTx;
10+
transaction: any; // Same type as UnsignedSweepPrebuildTx
11+
isLastSignature: boolean;
12+
replayProtectionOptions: ReplayProtectionOptions | undefined;
13+
}) {
14+
const decoratedSignedTx = { ...signedTx };
15+
if (signedTx.signatures) {
16+
decoratedSignedTx.isFullSigned = true;
17+
}
18+
if (transaction.feeInfo) {
19+
decoratedSignedTx.feeInfo = transaction.feeInfo;
20+
}
21+
if (transaction.coin) {
22+
decoratedSignedTx.coin = transaction.coin;
23+
}
24+
if (transaction.gasPrice) {
25+
decoratedSignedTx.gasPrice = transaction.gasPrice;
26+
}
27+
if (transaction.gasLimit) {
28+
decoratedSignedTx.gasLimit = transaction.gasLimit;
29+
}
30+
31+
if (transaction.eip1559) {
32+
decoratedSignedTx.eip1559 = transaction.eip1559;
33+
}
34+
if (transaction.gasPrice && transaction.gasLimit) {
35+
decoratedSignedTx.feesUsed = {
36+
gasPrice: transaction.gasPrice,
37+
gasLimit: transaction.gasLimit,
38+
};
39+
}
40+
if (transaction.isEvmBasedCrossChainRecovery) {
41+
decoratedSignedTx.isEvmBasedCrossChainRecovery = transaction.isEvmBasedCrossChainRecovery;
42+
}
43+
if (transaction.amount) {
44+
decoratedSignedTx.amount = transaction.amount;
45+
}
46+
if (!isLastSignature) {
47+
decoratedSignedTx.isHalfSigned = true;
48+
decoratedSignedTx.backupKeyNonce =
49+
'backupKeyNonce' in transaction
50+
? transaction.backupKeyNonce
51+
: transaction.nextContractSequenceId;
52+
decoratedSignedTx.walletContractAddress = transaction.walletContractAddress;
53+
decoratedSignedTx.replayProtectionOptions = getReplayProtectionOptions(replayProtectionOptions);
54+
}
55+
56+
return decoratedSignedTx;
57+
}
58+
59+
export function getReplayProtectionOptions(
60+
replayProtectionOptions: ReplayProtectionOptions | undefined = undefined,
61+
): ReplayProtectionOptions {
62+
return (
63+
replayProtectionOptions ?? {
64+
chain: 17000, // 1 if mainnet, 17000 if testnet
65+
hardfork: 'london',
66+
}
67+
);
68+
}
69+
70+
export function getDefaultMusigEthGasParams() {
71+
return {
72+
gasPrice: 20000000000,
73+
gasLimit: 200000,
74+
maxFeePerGas: 20000000000,
75+
maxPriorityFeePerGas: 10000000000,
76+
};
77+
}

src/types/transaction.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
11
import { type Recipient, type SignedTransaction } from 'bitgo';
22

3-
export type HalfSignedEthLikeRecoveryTx = SignedTransaction & {
4-
halfSigned: {
3+
export type SignedEthLikeRecoveryTx = SignedTransaction & {
4+
signatures?: string;
5+
halfSigned?: {
56
signatures?: string;
67
recipients?: Recipient[];
78
signingKeyNonce?: number;
89
backupKeyNonce?: number;
10+
expireTime?: number;
11+
txHex?: string;
912
};
13+
replayProtectionOptions?: ReplayProtectionOptions;
14+
recipients?: Recipient[];
15+
isFullSigned?: boolean;
16+
isHalfSigned?: boolean;
17+
backupKeyNonce?: number;
18+
walletContractAddress?: string;
19+
amount?: string;
20+
isEvmBasedCrossChainRecovery?: boolean;
21+
feesUsed?: {
22+
gasPrice: number;
23+
gasLimit: number;
24+
};
25+
gasLimit?: number;
26+
coin?: string;
27+
gasPrice?: number;
28+
feeInfo?: any;
29+
eip1559?: any;
30+
};
31+
32+
export type ReplayProtectionOptions = {
33+
chain: number; // 1 if mainnet, 17000 if testnet
34+
hardfork: string;
1035
};

0 commit comments

Comments
 (0)