Skip to content

Commit 0226803

Browse files
feat(examples): add BIP322 proof of address ownership example
Add example code for generating and verifying BIP322 generic message signatures for BitGo multi-sig wallets. This implementation demonstrates how to prove ownership of addresses from traditional (non-descriptor) wallets using Bitcoin's standard message signing protocol. Includes: - Comprehensive README explaining BIP322 concepts and usage - Sample code for creating and verifying message proofs - JSON configuration for address/message pairs Co-authored-by: llm-git <llm-git@ttll.de> TICKET: BTC-2998
1 parent e07fe2a commit 0226803

3 files changed

Lines changed: 390 additions & 0 deletions

File tree

examples/ts/btc/bip322/README.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# BIP322 Proof of Address Ownership
2+
3+
## What is BIP322?
4+
5+
[BIP322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki) (Bitcoin Improvement Proposal 322) is a standard for **generic message signing** in Bitcoin. It provides a way to cryptographically prove ownership of a Bitcoin address by signing an arbitrary message.
6+
7+
Unlike the legacy message signing approach (which only worked with P2PKH addresses), BIP322 supports all standard Bitcoin address types including:
8+
- P2SH (Pay-to-Script-Hash)
9+
- P2SH-P2WSH (Nested SegWit)
10+
- P2WSH (Native SegWit)
11+
- P2TR (Taproot)
12+
13+
## What is it used for?
14+
15+
BIP322 proofs are commonly used for:
16+
17+
1. **Proof of Reserves**: Exchanges and custodians can prove they control certain addresses without moving funds.
18+
19+
2. **Address Verification**: Verify that a counterparty owns an address before sending funds to them.
20+
21+
3. **Identity Verification**: Associate a Bitcoin address with an identity or account.
22+
23+
4. **Audit Compliance**: Provide cryptographic evidence of address ownership for regulatory or audit purposes.
24+
25+
5. **Dispute Resolution**: Prove ownership of funds in case of disputes.
26+
27+
## How to Use This Example
28+
29+
### Prerequisites
30+
31+
1. A BitGo account with API access
32+
2. A **traditional multi-sig wallet** (NOT a descriptor wallet)
33+
3. At least one address created on the wallet
34+
4. Node.js and the BitGoJS SDK installed
35+
36+
### Important Limitation
37+
38+
> **WARNING**: This example does NOT work with descriptor wallets. Only use this with traditional BitGo multi-sig wallets that have keychains with standard derivation paths.
39+
40+
### Step-by-Step Instructions
41+
42+
1. **Configure the example** by editing `verifyProof.ts`:
43+
```typescript
44+
// Set your environment: 'prod' for mainnet, 'test' for testnet
45+
const environment: 'prod' | 'test' = 'test';
46+
47+
// Set the coin: 'btc' for mainnet, 'tbtc4' for testnet
48+
const coin = 'tbtc4';
49+
50+
// Set your BitGo access token
51+
const accessToken = 'YOUR_ACCESS_TOKEN';
52+
53+
// Set your wallet ID
54+
const walletId = 'YOUR_WALLET_ID';
55+
56+
// Set your wallet passphrase
57+
const walletPassphrase = 'YOUR_WALLET_PASSPHRASE';
58+
```
59+
60+
2. **Edit `messages.json`** with the addresses and messages you want to prove:
61+
```json
62+
[
63+
{
64+
"address": "tb1q...",
65+
"message": "I own this address on 2025-02-02"
66+
},
67+
{
68+
"address": "2N...",
69+
"message": "Proof of ownership for audit"
70+
}
71+
]
72+
```
73+
74+
Each entry must contain:
75+
- `address`: A valid address that belongs to your wallet
76+
- `message`: The arbitrary message to sign (can be any string)
77+
78+
3. **Run the example**:
79+
```bash
80+
cd examples/ts/btc/bip322
81+
npx ts-node verifyProof.ts
82+
```
83+
84+
### What the Example Does
85+
86+
1. **Loads** the address/message pairs from `messages.json`
87+
2. **Fetches** the wallet and its keychains from BitGo
88+
3. **Gets address info** for each address to obtain the chain and index (needed for pubkey derivation)
89+
4. **Derives the script type** from the chain code (e.g., chain 10/11 = P2SH-P2WSH)
90+
5. **Derives the public keys** for each address using the wallet's keychains
91+
6. **Creates the BIP322 proof** by calling `wallet.sendMany()` with `type: 'bip322'`
92+
7. **Verifies the proof** using `bip322.assertBip322TxProof()` to ensure:
93+
- The transaction structure follows BIP322 requirements
94+
- The signatures are valid for the derived public keys
95+
- The message is correctly encoded in the transaction
96+
97+
### Expected Output
98+
99+
```
100+
Environment: test
101+
Coin: tbtc4
102+
Wallet ID: abc123...
103+
104+
Loaded 1 message(s) to prove:
105+
1. Address: tb1q...
106+
Message: I own this address
107+
108+
Fetching wallet...
109+
Wallet label: My Test Wallet
110+
111+
Fetching keychains...
112+
Retrieved wallet public keys
113+
114+
Building message info from address data...
115+
Getting address info for: tb1q...
116+
Chain: 20, Index: 0, ScriptType: p2wsh
117+
118+
Creating BIP322 proof via sendMany...
119+
BIP322 proof created successfully
120+
121+
Verifying BIP322 proof...
122+
Transaction proof verified successfully!
123+
124+
============================================
125+
BIP322 PROOF VERIFICATION COMPLETE
126+
============================================
127+
Verified 1 address/message pair(s):
128+
129+
1. Address: tb1q...
130+
Message: "I own this address"
131+
Script Type: p2wsh
132+
133+
All proofs are valid. The wallet controls the specified addresses.
134+
```
135+
136+
## Chain Codes and Script Types
137+
138+
BitGo uses chain codes to determine the address script type:
139+
140+
| Chain Code | Address Type | Description |
141+
|------------|--------------|-------------|
142+
| 0, 1 | P2SH | Legacy wrapped multi-sig |
143+
| 10, 11 | P2SH-P2WSH | Nested SegWit (compatible) |
144+
| 20, 21 | P2WSH | Native SegWit |
145+
| 30, 31 | P2TR | Taproot script path |
146+
| 40, 41 | P2TR-Musig2 | Taproot key path (MuSig2) |
147+
148+
Even chain codes (0, 10, 20, 30, 40) are for external/receive addresses.
149+
Odd chain codes (1, 11, 21, 31, 41) are for internal/change addresses.
150+
151+
## Troubleshooting
152+
153+
### "Address is missing chain or index information"
154+
The address may not belong to this wallet, or it may be from a descriptor wallet which is not supported.
155+
156+
### "Expected 3 keychains for multi-sig wallet"
157+
Ensure you're using a traditional BitGo multi-sig wallet, not a TSS or descriptor wallet.
158+
159+
### "No transaction hex found in sendMany result"
160+
The BIP322 proof request may have failed. Check the error details in the response.
161+
162+
## References
163+
164+
- [BIP322 Specification](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki)
165+
- [BitGo API Documentation](https://developers.bitgo.com/)
166+
- [BitGoJS SDK](https://github.com/BitGo/BitGoJS)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"address": "YOUR_WALLET_ADDRESS_HERE",
4+
"message": "I own this address"
5+
},
6+
{
7+
"address": "YOUR_OTHER_WALLET_ADDRESS_HERE",
8+
"message": "I also own this address"
9+
}
10+
]
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/**
2+
* Verify a BIP322 proof of address ownership from a BitGo multi-sig wallet.
3+
*
4+
* This example demonstrates how to:
5+
* 1. Get a wallet by ID
6+
* 2. Read address/message pairs from messages.json
7+
* 3. Get address information to obtain chain and index for pubkey derivation
8+
* 4. Create a BIP322 proof using sendMany with type 'bip322'
9+
* 5. Verify the proof using bip322.assertBip322TxProof
10+
*
11+
* IMPORTANT: This example does NOT work with descriptor wallets.
12+
* Only use this with traditional BitGo multi-sig wallets.
13+
*
14+
* Copyright 2025, BitGo, Inc. All Rights Reserved.
15+
*/
16+
17+
import * as fs from 'fs';
18+
import * as path from 'path';
19+
import { BitGo } from 'bitgo';
20+
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
21+
import * as utxolib from '@bitgo/utxo-lib';
22+
import { bip322 } from '@bitgo/utxo-core';
23+
import { BIP32Factory, ecc } from '@bitgo/secp256k1';
24+
25+
// ============================================================================
26+
// CONFIGURATION - Set these values before running
27+
// ============================================================================
28+
29+
// Set your environment: 'prod' for mainnet, 'test' for testnet
30+
const environment: 'prod' | 'test' = 'test';
31+
32+
// Set the coin: 'btc' for mainnet, 'tbtc4' for testnet
33+
const coin = 'tbtc4';
34+
35+
// Set your BitGo access token
36+
const accessToken = '';
37+
38+
// Set your wallet ID
39+
const walletId = '';
40+
41+
// Set your wallet passphrase for signing
42+
const walletPassphrase = '';
43+
44+
// Set the OTP code. If you dont need one, set it to undefined.
45+
const otp: string | undefined = undefined;
46+
47+
// ============================================================================
48+
// TYPES
49+
// ============================================================================
50+
51+
interface MessageEntry {
52+
address: string;
53+
message: string;
54+
}
55+
56+
async function main(): Promise<void> {
57+
// Validate configuration
58+
if (!accessToken) {
59+
throw new Error('Please set your accessToken in the configuration section');
60+
}
61+
if (!walletId) {
62+
throw new Error('Please set your walletId in the configuration section');
63+
}
64+
if (!walletPassphrase) {
65+
throw new Error('Please set your walletPassphrase in the configuration section');
66+
}
67+
68+
// Initialize BitGo SDK
69+
const bitgo = new BitGo({ env: environment });
70+
bitgo.authenticateWithAccessToken({ accessToken });
71+
if (otp) {
72+
const unlock = await bitgo.unlock({ otp, duration: 3600 });
73+
if (!unlock) {
74+
console.log('We did not unlock.');
75+
throw new Error();
76+
}
77+
}
78+
79+
const baseCoin = bitgo.coin(coin);
80+
81+
console.log(`Environment: ${environment}`);
82+
console.log(`Coin: ${coin}`);
83+
console.log(`Wallet ID: ${walletId}`);
84+
85+
// Read messages from JSON file
86+
const messagesPath = path.join(__dirname, 'messages.json');
87+
const messagesContent = fs.readFileSync(messagesPath, 'utf-8');
88+
const messages: MessageEntry[] = JSON.parse(messagesContent);
89+
90+
if (!Array.isArray(messages) || messages.length === 0) {
91+
throw new Error('messages.json must contain an array of {address, message} objects');
92+
}
93+
94+
console.log(`\nLoaded ${messages.length} message(s) to prove:`);
95+
messages.forEach((m, i) => {
96+
console.log(` ${i + 1}. Address: ${m.address}`);
97+
console.log(` Message: ${m.message}`);
98+
});
99+
100+
// Get the wallet
101+
console.log('\nFetching wallet...');
102+
const wallet = await baseCoin.wallets().get({ id: walletId });
103+
console.log(`Wallet label: ${wallet.label()}`);
104+
105+
// Get keychains for the wallet (needed for deriving pubkeys)
106+
console.log('\nFetching keychains...');
107+
const keychains = await baseCoin.keychains().getKeysForSigning({ wallet });
108+
const xpubs = keychains.map((k) => {
109+
if (!k.pub) {
110+
throw new Error('Keychain missing public key');
111+
}
112+
return k.pub;
113+
});
114+
console.log('Retrieved wallet public keys');
115+
116+
// Create RootWalletKeys from xpubs for derivation
117+
const bip32 = BIP32Factory(ecc);
118+
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(
119+
xpubs.map((xpub) => bip32.fromBase58(xpub)) as utxolib.bitgo.Triple<utxolib.BIP32Interface>
120+
);
121+
122+
// Build messageInfo array by getting address details for each message
123+
console.log('\nBuilding message info from address data...');
124+
const messageInfo: bip322.MessageInfo[] = [];
125+
126+
for (const entry of messages) {
127+
// Get address information from wallet to obtain chain and index
128+
console.log(` Getting address info for: ${entry.address}`);
129+
const addressInfo = await wallet.getAddress({ address: entry.address });
130+
131+
if (addressInfo.chain === undefined || addressInfo.index === undefined) {
132+
throw new Error(`Address ${entry.address} is missing chain or index information`);
133+
}
134+
135+
const chain = addressInfo.chain as utxolib.bitgo.ChainCode;
136+
const index = addressInfo.index;
137+
138+
// Derive scriptType from chain
139+
const scriptType = utxolib.bitgo.scriptTypeForChain(chain);
140+
141+
// Derive pubkeys for this address using chain and index
142+
const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chain, index);
143+
const pubkeys = derivedKeys.publicKeys.map((pk) => pk.toString('hex'));
144+
145+
console.log(` Chain: ${chain}, Index: ${index}, ScriptType: ${scriptType}`);
146+
147+
messageInfo.push({
148+
address: entry.address,
149+
message: entry.message,
150+
pubkeys,
151+
scriptType,
152+
});
153+
}
154+
155+
console.log('\nCreating BIP322 proof via sendMany...');
156+
const sendManyResult = await wallet.sendMany({
157+
recipients: [],
158+
messages: messages,
159+
walletPassphrase,
160+
});
161+
162+
console.log('BIP322 proof created successfully');
163+
164+
// Extract the signed transaction from the result
165+
// The result should contain the fully signed PSBT or transaction hex
166+
const txHex = sendManyResult.txHex || sendManyResult.tx;
167+
if (!txHex) {
168+
throw new Error('No transaction hex found in sendMany result');
169+
}
170+
171+
console.log('\nVerifying BIP322 proof...');
172+
173+
// Parse the transaction and verify
174+
const network = (baseCoin as AbstractUtxoCoin).network;
175+
176+
// Check if it's a PSBT or raw transaction
177+
if (utxolib.bitgo.isPsbt(txHex)) {
178+
// Parse as PSBT
179+
const psbt = utxolib.bitgo.createPsbtFromHex(txHex, network);
180+
bip322.assertBip322PsbtProof(psbt, messageInfo);
181+
console.log('PSBT proof verified successfully!');
182+
} else {
183+
// Parse as raw transaction
184+
const tx = utxolib.bitgo.createTransactionFromHex<bigint>(txHex, network, { amountType: 'bigint' });
185+
bip322.assertBip322TxProof(tx, messageInfo);
186+
console.log('Transaction proof verified successfully!');
187+
}
188+
189+
// Display summary
190+
console.log('\n============================================');
191+
console.log('BIP322 PROOF VERIFICATION COMPLETE');
192+
console.log('============================================');
193+
console.log(`Verified ${messageInfo.length} address/message pair(s):`);
194+
messageInfo.forEach((info, i) => {
195+
console.log(`\n${i + 1}. Address: ${info.address}`);
196+
console.log(` Message: "${info.message}"`);
197+
console.log(` Script Type: ${info.scriptType}`);
198+
});
199+
console.log('\nAll proofs are valid. The wallet controls the specified addresses.');
200+
}
201+
202+
// Run the example
203+
main()
204+
.then(() => {
205+
console.log('\nExample completed successfully.');
206+
process.exit(0);
207+
})
208+
.catch((e) => {
209+
console.error('\nExample failed with error:', e.message);
210+
if (e.result) {
211+
console.error('API Error details:', JSON.stringify(e.result, null, 2));
212+
}
213+
process.exit(1);
214+
});

0 commit comments

Comments
 (0)