Skip to content

Commit 2a18802

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): implement BIP-322 message signing for fixed-script wallets
Add full implementation of BIP-0322 generic signed message format: - Create core Rust implementation with tagged message hashing - Add WASM bindings for JavaScript/TypeScript usage - Support all wallet script types (p2sh, p2shP2wsh, p2wsh, p2tr) - Provide comprehensive verification API for both PSBTs and transactions - Include TypeScript API definitions and comprehensive tests Issue: BTC-2916 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent a47ee16 commit 2a18802

11 files changed

Lines changed: 1979 additions & 5 deletions

File tree

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
/**
2+
* BIP-0322 Generic Signed Message Format
3+
*
4+
* This module implements BIP-0322 for BitGo fixed-script wallets.
5+
* It allows proving control of wallet addresses by signing arbitrary messages.
6+
*
7+
* @example
8+
* ```typescript
9+
* import { bip322, fixedScriptWallet } from '@bitgo/wasm-utxo';
10+
*
11+
* // Create wallet keys
12+
* const walletKeys = fixedScriptWallet.RootWalletKeys.from([userXpub, backupXpub, bitgoXpub]);
13+
*
14+
* // Create an empty PSBT for BIP-0322 (version 0 required)
15+
* const psbt = BitGoPsbt.createEmpty("bitcoin", walletKeys, { version: 0 });
16+
*
17+
* // Add BIP-0322 inputs
18+
* const idx0 = bip322.addBip322Input(psbt, {
19+
* message: "Hello, World!",
20+
* scriptId: { chain: 10, index: 0 },
21+
* rootWalletKeys: walletKeys,
22+
* });
23+
*
24+
* // Sign the input
25+
* psbt.sign(idx0, userXpriv);
26+
* psbt.sign(idx0, bitgoXpriv);
27+
*
28+
* // Verify the input
29+
* bip322.verifyBip322PsbtInput(psbt, idx0, {
30+
* message: "Hello, World!",
31+
* scriptId: { chain: 10, index: 0 },
32+
* rootWalletKeys: walletKeys,
33+
* });
34+
* ```
35+
*/
36+
37+
import { Bip322Namespace } from "../wasm/wasm_utxo.js";
38+
import {
39+
BitGoPsbt,
40+
type NetworkName,
41+
type ScriptId,
42+
type SignPath,
43+
} from "../fixedScriptWallet/BitGoPsbt.js";
44+
import { type WalletKeysArg, RootWalletKeys } from "../fixedScriptWallet/RootWalletKeys.js";
45+
import { type OutputScriptType } from "../fixedScriptWallet/scriptType.js";
46+
import { Transaction } from "../transaction.js";
47+
48+
// Re-export OutputScriptType for backwards compatibility
49+
export type { OutputScriptType };
50+
51+
/**
52+
* Parameters for adding a BIP-0322 input to a PSBT
53+
*/
54+
export type AddBip322InputParams = {
55+
/** The message to sign (UTF-8 string) */
56+
message: string;
57+
/** The wallet script location (chain and index) */
58+
scriptId: ScriptId;
59+
/** The wallet's root keys */
60+
rootWalletKeys: WalletKeysArg;
61+
/**
62+
* Sign path for taproot inputs (required for p2tr/p2trMusig2).
63+
* Specifies which two keys will sign the message.
64+
*/
65+
signPath?: SignPath;
66+
/** Custom tag for message hashing (default: "BIP0322-signed-message") */
67+
tag?: string;
68+
};
69+
70+
/**
71+
* Parameters for verifying a BIP-0322 input
72+
*/
73+
export type VerifyBip322InputParams = {
74+
/** The message that was signed */
75+
message: string;
76+
/** The wallet script location (chain and index) */
77+
scriptId: ScriptId;
78+
/** The wallet's root keys */
79+
rootWalletKeys: WalletKeysArg;
80+
/** Custom tag if one was used during signing */
81+
tag?: string;
82+
};
83+
84+
/**
85+
* Parameters for verifying a BIP-0322 transaction input
86+
*/
87+
export type VerifyBip322TxInputParams = VerifyBip322InputParams & {
88+
/** Network name (default: "bitcoin") */
89+
network?: NetworkName;
90+
};
91+
92+
/**
93+
* Add a BIP-0322 message input to an existing BitGoPsbt
94+
*
95+
* The PSBT must have version 0 per BIP-0322 specification. Use
96+
* `BitGoPsbt.createEmpty(network, walletKeys, { version: 0 })` to create one.
97+
*
98+
* On the first input added, this also adds the required OP_RETURN output.
99+
*
100+
* @param psbt - The BitGoPsbt to add the input to (must have version 0)
101+
* @param params - Input parameters including message, scriptId, and wallet keys
102+
* @returns The index of the added input
103+
*
104+
* @example
105+
* ```typescript
106+
* // Create a BIP-0322 PSBT
107+
* const psbt = BitGoPsbt.createEmpty("bitcoin", walletKeys, { version: 0 });
108+
*
109+
* // Add inputs
110+
* const idx0 = bip322.addBip322Input(psbt, {
111+
* message: "I control this address",
112+
* scriptId: { chain: 10, index: 5 },
113+
* rootWalletKeys: walletKeys,
114+
* });
115+
*
116+
* // Sign with user and bitgo keys
117+
* psbt.sign(idx0, userXpriv);
118+
* psbt.sign(idx0, bitgoXpriv);
119+
* ```
120+
*/
121+
export function addBip322Input(psbt: BitGoPsbt, params: AddBip322InputParams): number {
122+
const keys = RootWalletKeys.from(params.rootWalletKeys);
123+
124+
return Bip322Namespace.add_bip322_input(
125+
psbt.wasm,
126+
params.message,
127+
params.scriptId.chain,
128+
params.scriptId.index,
129+
keys.wasm,
130+
params.signPath?.signer,
131+
params.signPath?.cosigner,
132+
params.tag,
133+
);
134+
}
135+
136+
/**
137+
* Verify a single input of a BIP-0322 transaction proof
138+
*
139+
* This verifies that the specified input correctly proves control of the
140+
* wallet address corresponding to the given message.
141+
*
142+
* @param tx - The signed transaction
143+
* @param inputIndex - The index of the input to verify
144+
* @param params - Verification parameters including message, scriptId, and wallet keys
145+
* @throws Error if verification fails
146+
*
147+
* @example
148+
* ```typescript
149+
* // Extract and verify the transaction
150+
* psbt.finalizeAllInputs();
151+
* const txBytes = psbt.extractTransaction();
152+
* const tx = Transaction.fromBytes(txBytes, "bitcoin");
153+
*
154+
* bip322.verifyBip322TxInput(tx, 0, {
155+
* message: "Hello, World!",
156+
* scriptId: { chain: 10, index: 0 },
157+
* rootWalletKeys: walletKeys,
158+
* network: "bitcoin",
159+
* });
160+
* ```
161+
*/
162+
export function verifyBip322TxInput(
163+
tx: Transaction,
164+
inputIndex: number,
165+
params: VerifyBip322TxInputParams,
166+
): void {
167+
const keys = RootWalletKeys.from(params.rootWalletKeys);
168+
const network = params.network ?? "bitcoin";
169+
170+
Bip322Namespace.verify_bip322_tx_input(
171+
tx.wasm,
172+
inputIndex,
173+
params.message,
174+
params.scriptId.chain,
175+
params.scriptId.index,
176+
keys.wasm,
177+
network,
178+
params.tag,
179+
);
180+
}
181+
182+
/** Signer key name */
183+
export type SignerName = "user" | "backup" | "bitgo";
184+
185+
/** Triple of hex-encoded pubkeys [user, backup, bitgo] */
186+
export type PubkeyTriple = [string, string, string];
187+
188+
/**
189+
* Parameters for verifying a BIP-0322 input with pubkeys
190+
*/
191+
export type VerifyBip322WithPubkeysParams = {
192+
/** The message that was signed */
193+
message: string;
194+
/** Hex-encoded pubkeys [user, backup, bitgo] */
195+
pubkeys: PubkeyTriple;
196+
/** Script type */
197+
scriptType: OutputScriptType;
198+
/** For taproot types, whether script path was used */
199+
isScriptPath?: boolean;
200+
/** Custom tag if one was used during signing */
201+
tag?: string;
202+
};
203+
204+
/**
205+
* Parameters for verifying a BIP-0322 transaction input with pubkeys
206+
*/
207+
export type VerifyBip322TxWithPubkeysParams = VerifyBip322WithPubkeysParams;
208+
209+
/**
210+
* Verify a single input of a BIP-0322 PSBT proof
211+
*
212+
* This verifies that the specified input correctly proves control of the
213+
* wallet address by checking:
214+
* - The PSBT structure follows BIP-0322 (version 0, OP_RETURN output)
215+
* - The input references the correct virtual to_spend transaction
216+
* - At least one valid signature exists from the wallet keys
217+
*
218+
* @param psbt - The signed PSBT
219+
* @param inputIndex - The index of the input to verify
220+
* @param params - Verification parameters including message, scriptId, and wallet keys
221+
* @returns An array of signer names ("user", "backup", "bitgo") that have valid signatures
222+
* @throws Error if verification fails or no valid signatures found
223+
*
224+
* @example
225+
* ```typescript
226+
* // Verify the signed PSBT input
227+
* const signers = bip322.verifyBip322PsbtInput(psbt, 0, {
228+
* message: "Hello, World!",
229+
* scriptId: { chain: 10, index: 0 },
230+
* rootWalletKeys: walletKeys,
231+
* });
232+
* console.log(signers); // ["user", "bitgo"]
233+
* ```
234+
*/
235+
export function verifyBip322PsbtInput(
236+
psbt: BitGoPsbt,
237+
inputIndex: number,
238+
params: VerifyBip322InputParams,
239+
): SignerName[] {
240+
const keys = RootWalletKeys.from(params.rootWalletKeys);
241+
242+
return Bip322Namespace.verify_bip322_psbt_input(
243+
psbt.wasm,
244+
inputIndex,
245+
params.message,
246+
params.scriptId.chain,
247+
params.scriptId.index,
248+
keys.wasm,
249+
params.tag,
250+
) as SignerName[];
251+
}
252+
253+
/**
254+
* Verify a single input of a BIP-0322 PSBT proof using pubkeys directly
255+
*
256+
* This verifies that the specified input correctly proves control of the
257+
* wallet address by checking:
258+
* - The PSBT structure follows BIP-0322 (version 0, OP_RETURN output)
259+
* - The input references the correct virtual to_spend transaction
260+
* - At least one valid signature exists from the provided pubkeys
261+
*
262+
* @param psbt - The signed PSBT
263+
* @param inputIndex - The index of the input to verify
264+
* @param params - Verification parameters including message, pubkeys, and script type
265+
* @returns An array of pubkey indices (0, 1, 2) that have valid signatures
266+
* @throws Error if verification fails or no valid signatures found
267+
*
268+
* @example
269+
* ```typescript
270+
* // Verify the signed PSBT input with pubkeys
271+
* const signerIndices = bip322.verifyBip322PsbtInputWithPubkeys(psbt, 0, {
272+
* message: "Hello, World!",
273+
* pubkeys: [userPubkey, backupPubkey, bitgoPubkey],
274+
* scriptType: "p2shP2wsh",
275+
* });
276+
* console.log(signerIndices); // [0, 2] for user+bitgo
277+
* ```
278+
*/
279+
export function verifyBip322PsbtInputWithPubkeys(
280+
psbt: BitGoPsbt,
281+
inputIndex: number,
282+
params: VerifyBip322WithPubkeysParams,
283+
): number[] {
284+
return Array.from(
285+
Bip322Namespace.verify_bip322_psbt_input_with_pubkeys(
286+
psbt.wasm,
287+
inputIndex,
288+
params.message,
289+
params.pubkeys,
290+
params.scriptType,
291+
params.isScriptPath,
292+
params.tag,
293+
),
294+
);
295+
}
296+
297+
/**
298+
* Verify a single input of a BIP-0322 transaction proof using pubkeys directly
299+
*
300+
* This verifies that the specified input correctly proves control of the
301+
* wallet address corresponding to the given message.
302+
*
303+
* @param tx - The signed transaction
304+
* @param inputIndex - The index of the input to verify
305+
* @param params - Verification parameters including message, pubkeys, and script type
306+
* @returns An array of pubkey indices (0, 1, 2) that have valid signatures
307+
* @throws Error if verification fails
308+
*
309+
* @example
310+
* ```typescript
311+
* // Verify the signed transaction input with pubkeys
312+
* const signerIndices = bip322.verifyBip322TxInputWithPubkeys(tx, 0, {
313+
* message: "Hello, World!",
314+
* pubkeys: [userPubkey, backupPubkey, bitgoPubkey],
315+
* scriptType: "p2wsh",
316+
* });
317+
* console.log(signerIndices); // [0, 2] for user+bitgo
318+
* ```
319+
*/
320+
export function verifyBip322TxInputWithPubkeys(
321+
tx: Transaction,
322+
inputIndex: number,
323+
params: VerifyBip322TxWithPubkeysParams,
324+
): number[] {
325+
return Array.from(
326+
Bip322Namespace.verify_bip322_tx_input_with_pubkeys(
327+
tx.wasm,
328+
inputIndex,
329+
params.message,
330+
params.pubkeys,
331+
params.scriptType,
332+
params.isScriptPath,
333+
params.tag,
334+
),
335+
);
336+
}

packages/wasm-utxo/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ void wasm;
88
// and to make imports more explicit (e.g., `import { address } from '@bitgo/wasm-utxo'`)
99
export * as address from "./address.js";
1010
export * as ast from "./ast/index.js";
11+
export * as bip322 from "./bip322/index.js";
1112
export * as utxolibCompat from "./utxolibCompat.js";
1213
export * as fixedScriptWallet from "./fixedScriptWallet/index.js";
1314
export * as bip32 from "./bip32.js";

0 commit comments

Comments
 (0)