-
Notifications
You must be signed in to change notification settings - Fork 302
Expand file tree
/
Copy pathsignPsbtWasm.ts
More file actions
148 lines (131 loc) · 5.45 KB
/
signPsbtWasm.ts
File metadata and controls
148 lines (131 loc) · 5.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import assert from 'assert';
import { BIP32, bip32, ECPair, fixedScriptWallet, getWasmUtxoVersion } from '@bitgo/wasm-utxo';
import { toWasmBIP32 } from '../../wasmUtil';
import { BulkSigningError, InputSigningError, TransactionSigningError } from './SigningError';
import { Musig2Participant } from './musig2';
export type ReplayProtectionKeys = {
publicKeys: (Uint8Array | ECPair)[];
};
/**
* Key Value: Unsigned tx id => PSBT
* It is used to cache PSBTs with taproot key path (MuSig2) inputs during external express signer is activated.
* Reason: MuSig2 signer secure nonce is cached in the BitGoPsbt object. It will be required during the signing step.
* For more info, check SignTransactionOptions.signingStep
*/
const PSBT_CACHE_WASM = new Map<string, fixedScriptWallet.BitGoPsbt>();
function hasKeyPathSpendInput(
tx: fixedScriptWallet.BitGoPsbt,
rootWalletKeys: fixedScriptWallet.RootWalletKeys,
replayProtection: ReplayProtectionKeys
): boolean {
const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection });
return parsed.inputs.some((input) => input.scriptType === 'p2trMusig2KeyPath');
}
/**
* Sign all inputs of a PSBT and verify signatures after signing.
* Uses bulk signing for performance (signs all matching inputs in one pass).
* Collects and logs signing errors and verification errors, throws error in the end if any of them failed.
*/
export function signAndVerifyPsbtWasm(
tx: fixedScriptWallet.BitGoPsbt,
signerKeychain: bip32.BIP32Interface | BIP32,
rootWalletKeys: fixedScriptWallet.RootWalletKeys,
replayProtection: ReplayProtectionKeys
): fixedScriptWallet.BitGoPsbt {
const wasmSigner = toWasmBIP32(signerKeychain);
// Bulk sign all wallet inputs (ECDSA + MuSig2) - much faster than per-input signing
try {
tx.sign(wasmSigner);
} catch (e) {
throw new BulkSigningError(e);
}
// Verify signatures for all signed inputs (still per-input for granular error reporting)
const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection });
const verifyErrors: InputSigningError<bigint>[] = [];
parsed.inputs.forEach((input, inputIndex) => {
if (input.scriptType === 'p2shP2pk') {
// Skip replay protection inputs - they are platform signed only
return;
}
const outputId = `${input.previousOutput.txid}:${input.previousOutput.vout}`;
try {
if (!tx.verifySignature(inputIndex, wasmSigner)) {
verifyErrors.push(
new InputSigningError(inputIndex, input.scriptType, { id: outputId }, new Error('invalid signature'))
);
}
} catch (e) {
verifyErrors.push(new InputSigningError<bigint>(inputIndex, input.scriptType, { id: outputId }, e));
}
});
if (verifyErrors.length) {
throw new TransactionSigningError([], verifyErrors);
}
const versionInfo = getWasmUtxoVersion();
const versionPayload = new TextEncoder().encode(
JSON.stringify({
version: versionInfo.version,
gitHash: versionInfo.gitHash,
})
);
tx.setKV({ type: 'bitgo', subtype: fixedScriptWallet.BitGoKeySubtype.WasmUtxoSignedWith }, versionPayload);
return tx;
}
export async function signPsbtWithMusig2ParticipantWasm(
coin: Musig2Participant<fixedScriptWallet.BitGoPsbt>,
tx: fixedScriptWallet.BitGoPsbt,
signerKeychain: bip32.BIP32Interface | undefined,
rootWalletKeys: fixedScriptWallet.RootWalletKeys,
params: {
replayProtection: ReplayProtectionKeys;
signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined;
walletId: string | undefined;
}
): Promise<fixedScriptWallet.BitGoPsbt> {
const wasmSigner = signerKeychain ? toWasmBIP32(signerKeychain) : undefined;
if (hasKeyPathSpendInput(tx, rootWalletKeys, params.replayProtection)) {
switch (params.signingStep) {
case 'signerNonce':
assert(wasmSigner);
tx.generateMusig2Nonces(wasmSigner);
PSBT_CACHE_WASM.set(tx.unsignedTxId(), tx);
return tx;
case 'cosignerNonce':
assert(params.walletId, 'walletId is required for MuSig2 bitgo nonce');
return await coin.getMusig2Nonces(tx, params.walletId);
case 'signerSignature': {
const txId = tx.unsignedTxId();
const cachedPsbt = PSBT_CACHE_WASM.get(txId);
assert(
cachedPsbt,
`Psbt is missing from txCache (cache size ${PSBT_CACHE_WASM.size}).
This may be due to the request being routed to a different BitGo-Express instance that for signing step 'signerNonce'.`
);
PSBT_CACHE_WASM.delete(txId);
cachedPsbt.combineMusig2Nonces(tx);
tx = cachedPsbt;
break;
}
default:
// this instance is not an external signer
assert(params.walletId, 'walletId is required for MuSig2 bitgo nonce');
assert(wasmSigner);
tx.generateMusig2Nonces(wasmSigner);
const response = await coin.getMusig2Nonces(tx, params.walletId);
tx.combineMusig2Nonces(response);
break;
}
} else {
switch (params.signingStep) {
case 'signerNonce':
case 'cosignerNonce':
/**
* In certain cases, the caller of this method may not know whether the txHex contains a psbt with taproot key path spend input(s).
* Instead of throwing error, no-op and return the txHex. So that the caller can call this method in the same sequence.
*/
return tx;
}
}
assert(signerKeychain);
return signAndVerifyPsbtWasm(tx, signerKeychain, rootWalletKeys, params.replayProtection);
}