Skip to content

Commit 79aad00

Browse files
Merge pull request #8047 from BitGo/BTC-2998.script
feat(examples): add BIP322 proof of address ownership example
2 parents 384410b + 9b17a66 commit 79aad00

File tree

3 files changed

+394
-0
lines changed

3 files changed

+394
-0
lines changed

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: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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+
* This works for Hot wallets only.
15+
*
16+
* Supports all address types except for Taproot Musig2.
17+
*
18+
* Copyright 2025, BitGo, Inc. All Rights Reserved.
19+
*/
20+
21+
import * as fs from 'fs';
22+
import * as path from 'path';
23+
import { BitGo } from 'bitgo';
24+
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
25+
import * as utxolib from '@bitgo/utxo-lib';
26+
import { bip322 } from '@bitgo/utxo-core';
27+
import { BIP32Factory, ecc } from '@bitgo/secp256k1';
28+
29+
// ============================================================================
30+
// CONFIGURATION - Set these values before running
31+
// ============================================================================
32+
33+
// Set your environment: 'prod' for mainnet, 'test' for testnet
34+
const environment: 'prod' | 'test' = 'test';
35+
36+
// Set the coin: 'btc' for mainnet, 'tbtc4' for testnet
37+
const coin = 'tbtc4';
38+
39+
// Set your BitGo access token
40+
const accessToken = '';
41+
42+
// Set your wallet ID
43+
const walletId = '';
44+
45+
// Set your wallet passphrase for signing
46+
const walletPassphrase = '';
47+
48+
// Set the OTP code. If you dont need one, set it to undefined.
49+
const otp: string | undefined = undefined;
50+
51+
// ============================================================================
52+
// TYPES
53+
// ============================================================================
54+
55+
interface MessageEntry {
56+
address: string;
57+
message: string;
58+
}
59+
60+
async function main(): Promise<void> {
61+
// Validate configuration
62+
if (!accessToken) {
63+
throw new Error('Please set your accessToken in the configuration section');
64+
}
65+
if (!walletId) {
66+
throw new Error('Please set your walletId in the configuration section');
67+
}
68+
if (!walletPassphrase) {
69+
throw new Error('Please set your walletPassphrase in the configuration section');
70+
}
71+
72+
// Initialize BitGo SDK
73+
const bitgo = new BitGo({ env: environment });
74+
bitgo.authenticateWithAccessToken({ accessToken });
75+
if (otp) {
76+
const unlock = await bitgo.unlock({ otp, duration: 3600 });
77+
if (!unlock) {
78+
console.log('We did not unlock.');
79+
throw new Error();
80+
}
81+
}
82+
83+
const baseCoin = bitgo.coin(coin);
84+
85+
console.log(`Environment: ${environment}`);
86+
console.log(`Coin: ${coin}`);
87+
console.log(`Wallet ID: ${walletId}`);
88+
89+
// Read messages from JSON file
90+
const messagesPath = path.join(__dirname, 'messages.json');
91+
const messagesContent = fs.readFileSync(messagesPath, 'utf-8');
92+
const messages: MessageEntry[] = JSON.parse(messagesContent);
93+
94+
if (!Array.isArray(messages) || messages.length === 0) {
95+
throw new Error('messages.json must contain an array of {address, message} objects');
96+
}
97+
98+
console.log(`\nLoaded ${messages.length} message(s) to prove:`);
99+
messages.forEach((m, i) => {
100+
console.log(` ${i + 1}. Address: ${m.address}`);
101+
console.log(` Message: ${m.message}`);
102+
});
103+
104+
// Get the wallet
105+
console.log('\nFetching wallet...');
106+
const wallet = await baseCoin.wallets().get({ id: walletId });
107+
console.log(`Wallet label: ${wallet.label()}`);
108+
109+
// Get keychains for the wallet (needed for deriving pubkeys)
110+
console.log('\nFetching keychains...');
111+
const keychains = await baseCoin.keychains().getKeysForSigning({ wallet });
112+
const xpubs = keychains.map((k) => {
113+
if (!k.pub) {
114+
throw new Error('Keychain missing public key');
115+
}
116+
return k.pub;
117+
});
118+
console.log('Retrieved wallet public keys');
119+
120+
// Create RootWalletKeys from xpubs for derivation
121+
const bip32 = BIP32Factory(ecc);
122+
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(
123+
xpubs.map((xpub) => bip32.fromBase58(xpub)) as utxolib.bitgo.Triple<utxolib.BIP32Interface>
124+
);
125+
126+
// Build messageInfo array by getting address details for each message
127+
console.log('\nBuilding message info from address data...');
128+
const messageInfo: bip322.MessageInfo[] = [];
129+
130+
for (const entry of messages) {
131+
// Get address information from wallet to obtain chain and index
132+
console.log(` Getting address info for: ${entry.address}`);
133+
const addressInfo = await wallet.getAddress({ address: entry.address });
134+
135+
if (addressInfo.chain === undefined || addressInfo.index === undefined) {
136+
throw new Error(`Address ${entry.address} is missing chain or index information`);
137+
}
138+
139+
const chain = addressInfo.chain as utxolib.bitgo.ChainCode;
140+
const index = addressInfo.index;
141+
142+
// Derive scriptType from chain
143+
const scriptType = utxolib.bitgo.scriptTypeForChain(chain);
144+
145+
// Derive pubkeys for this address using chain and index
146+
const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chain, index);
147+
const pubkeys = derivedKeys.publicKeys.map((pk) => pk.toString('hex'));
148+
149+
console.log(` Chain: ${chain}, Index: ${index}, ScriptType: ${scriptType}`);
150+
151+
messageInfo.push({
152+
address: entry.address,
153+
message: entry.message,
154+
pubkeys,
155+
scriptType,
156+
});
157+
}
158+
159+
console.log('\nCreating BIP322 proof via sendMany...');
160+
const sendManyResult = await wallet.sendMany({
161+
recipients: [],
162+
messages: messages,
163+
walletPassphrase,
164+
});
165+
166+
console.log('BIP322 proof created successfully');
167+
168+
// Extract the signed transaction from the result
169+
// The result should contain the fully signed PSBT or transaction hex
170+
const txHex = sendManyResult.txHex || sendManyResult.tx;
171+
if (!txHex) {
172+
throw new Error('No transaction hex found in sendMany result');
173+
}
174+
175+
console.log('\nVerifying BIP322 proof...');
176+
177+
// Parse the transaction and verify
178+
const network = (baseCoin as AbstractUtxoCoin).network;
179+
180+
// Check if it's a PSBT or raw transaction
181+
if (utxolib.bitgo.isPsbt(txHex)) {
182+
// Parse as PSBT
183+
const psbt = utxolib.bitgo.createPsbtFromHex(txHex, network);
184+
bip322.assertBip322PsbtProof(psbt, messageInfo);
185+
console.log('PSBT proof verified successfully!');
186+
} else {
187+
// Parse as raw transaction
188+
const tx = utxolib.bitgo.createTransactionFromHex<bigint>(txHex, network, { amountType: 'bigint' });
189+
bip322.assertBip322TxProof(tx, messageInfo);
190+
console.log('Transaction proof verified successfully!');
191+
}
192+
193+
// Display summary
194+
console.log('\n============================================');
195+
console.log('BIP322 PROOF VERIFICATION COMPLETE');
196+
console.log('============================================');
197+
console.log(`Verified ${messageInfo.length} address/message pair(s):`);
198+
messageInfo.forEach((info, i) => {
199+
console.log(`\n${i + 1}. Address: ${info.address}`);
200+
console.log(` Message: "${info.message}"`);
201+
console.log(` Script Type: ${info.scriptType}`);
202+
});
203+
console.log('\nAll proofs are valid. The wallet controls the specified addresses.');
204+
}
205+
206+
// Run the example
207+
main()
208+
.then(() => {
209+
console.log('\nExample completed successfully.');
210+
process.exit(0);
211+
})
212+
.catch((e) => {
213+
console.error('\nExample failed with error:', e.message);
214+
if (e.result) {
215+
console.error('API Error details:', JSON.stringify(e.result, null, 2));
216+
}
217+
process.exit(1);
218+
});

0 commit comments

Comments
 (0)