Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions examples/ts/btc/bip322/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# BIP322 Proof of Address Ownership

## What is BIP322?

[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.

Unlike the legacy message signing approach (which only worked with P2PKH addresses), BIP322 supports all standard Bitcoin address types including:
- P2SH (Pay-to-Script-Hash)
- P2SH-P2WSH (Nested SegWit)
- P2WSH (Native SegWit)
- P2TR (Taproot)

## What is it used for?

BIP322 proofs are commonly used for:

1. **Proof of Reserves**: Exchanges and custodians can prove they control certain addresses without moving funds.

2. **Address Verification**: Verify that a counterparty owns an address before sending funds to them.

3. **Identity Verification**: Associate a Bitcoin address with an identity or account.

4. **Audit Compliance**: Provide cryptographic evidence of address ownership for regulatory or audit purposes.

5. **Dispute Resolution**: Prove ownership of funds in case of disputes.

## How to Use This Example

### Prerequisites

1. A BitGo account with API access
2. A **traditional multi-sig wallet** (NOT a descriptor wallet)
3. At least one address created on the wallet
4. Node.js and the BitGoJS SDK installed

### Important Limitation

> **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.

### Step-by-Step Instructions

1. **Configure the example** by editing `verifyProof.ts`:
```typescript
// Set your environment: 'prod' for mainnet, 'test' for testnet
const environment: 'prod' | 'test' = 'test';

// Set the coin: 'btc' for mainnet, 'tbtc4' for testnet
const coin = 'tbtc4';

// Set your BitGo access token
const accessToken = 'YOUR_ACCESS_TOKEN';

// Set your wallet ID
const walletId = 'YOUR_WALLET_ID';

// Set your wallet passphrase
const walletPassphrase = 'YOUR_WALLET_PASSPHRASE';
```

2. **Edit `messages.json`** with the addresses and messages you want to prove:
```json
[
{
"address": "tb1q...",
"message": "I own this address on 2025-02-02"
},
{
"address": "2N...",
"message": "Proof of ownership for audit"
}
]
```

Each entry must contain:
- `address`: A valid address that belongs to your wallet
- `message`: The arbitrary message to sign (can be any string)

3. **Run the example**:
```bash
cd examples/ts/btc/bip322
npx ts-node verifyProof.ts
```

### What the Example Does

1. **Loads** the address/message pairs from `messages.json`
2. **Fetches** the wallet and its keychains from BitGo
3. **Gets address info** for each address to obtain the chain and index (needed for pubkey derivation)
4. **Derives the script type** from the chain code (e.g., chain 10/11 = P2SH-P2WSH)
5. **Derives the public keys** for each address using the wallet's keychains
6. **Creates the BIP322 proof** by calling `wallet.sendMany()` with `type: 'bip322'`
7. **Verifies the proof** using `bip322.assertBip322TxProof()` to ensure:
- The transaction structure follows BIP322 requirements
- The signatures are valid for the derived public keys
- The message is correctly encoded in the transaction

### Expected Output

```
Environment: test
Coin: tbtc4
Wallet ID: abc123...

Loaded 1 message(s) to prove:
1. Address: tb1q...
Message: I own this address

Fetching wallet...
Wallet label: My Test Wallet

Fetching keychains...
Retrieved wallet public keys

Building message info from address data...
Getting address info for: tb1q...
Chain: 20, Index: 0, ScriptType: p2wsh

Creating BIP322 proof via sendMany...
BIP322 proof created successfully

Verifying BIP322 proof...
Transaction proof verified successfully!

============================================
BIP322 PROOF VERIFICATION COMPLETE
============================================
Verified 1 address/message pair(s):

1. Address: tb1q...
Message: "I own this address"
Script Type: p2wsh

All proofs are valid. The wallet controls the specified addresses.
```

## Chain Codes and Script Types

BitGo uses chain codes to determine the address script type:

| Chain Code | Address Type | Description |
|------------|--------------|-------------|
| 0, 1 | P2SH | Legacy wrapped multi-sig |
| 10, 11 | P2SH-P2WSH | Nested SegWit (compatible) |
| 20, 21 | P2WSH | Native SegWit |
| 30, 31 | P2TR | Taproot script path |
| 40, 41 | P2TR-Musig2 | Taproot key path (MuSig2) |

Even chain codes (0, 10, 20, 30, 40) are for external/receive addresses.
Odd chain codes (1, 11, 21, 31, 41) are for internal/change addresses.

## Troubleshooting

### "Address is missing chain or index information"
The address may not belong to this wallet, or it may be from a descriptor wallet which is not supported.

### "Expected 3 keychains for multi-sig wallet"
Ensure you're using a traditional BitGo multi-sig wallet, not a TSS or descriptor wallet.

### "No transaction hex found in sendMany result"
The BIP322 proof request may have failed. Check the error details in the response.

## References

- [BIP322 Specification](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki)
- [BitGo API Documentation](https://developers.bitgo.com/)
- [BitGoJS SDK](https://github.com/BitGo/BitGoJS)
10 changes: 10 additions & 0 deletions examples/ts/btc/bip322/messages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"address": "YOUR_WALLET_ADDRESS_HERE",
"message": "I own this address"
},
{
"address": "YOUR_OTHER_WALLET_ADDRESS_HERE",
"message": "I also own this address"
}
]
218 changes: 218 additions & 0 deletions examples/ts/btc/bip322/verifyProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/**
* Verify a BIP322 proof of address ownership from a BitGo multi-sig wallet.
*
* This example demonstrates how to:
* 1. Get a wallet by ID
* 2. Read address/message pairs from messages.json
* 3. Get address information to obtain chain and index for pubkey derivation
* 4. Create a BIP322 proof using sendMany with type 'bip322'
* 5. Verify the proof using bip322.assertBip322TxProof
*
* IMPORTANT: This example does NOT work with descriptor wallets.
* Only use this with traditional BitGo multi-sig wallets.
*
* This works for Hot wallets only.
*
* Supports all address types except for Taproot Musig2.
*
* Copyright 2025, BitGo, Inc. All Rights Reserved.
*/

import * as fs from 'fs';
import * as path from 'path';
import { BitGo } from 'bitgo';
import { AbstractUtxoCoin } from '@bitgo/abstract-utxo';
import * as utxolib from '@bitgo/utxo-lib';
import { bip322 } from '@bitgo/utxo-core';
import { BIP32Factory, ecc } from '@bitgo/secp256k1';

// ============================================================================
// CONFIGURATION - Set these values before running
// ============================================================================

// Set your environment: 'prod' for mainnet, 'test' for testnet
const environment: 'prod' | 'test' = 'test';

// Set the coin: 'btc' for mainnet, 'tbtc4' for testnet
const coin = 'tbtc4';

// Set your BitGo access token
const accessToken = '';

// Set your wallet ID
const walletId = '';

// Set your wallet passphrase for signing
const walletPassphrase = '';

// Set the OTP code. If you dont need one, set it to undefined.
const otp: string | undefined = undefined;

// ============================================================================
// TYPES
// ============================================================================

interface MessageEntry {
address: string;
message: string;
}

async function main(): Promise<void> {
// Validate configuration
if (!accessToken) {
throw new Error('Please set your accessToken in the configuration section');
}
if (!walletId) {
throw new Error('Please set your walletId in the configuration section');
}
if (!walletPassphrase) {
throw new Error('Please set your walletPassphrase in the configuration section');
}

// Initialize BitGo SDK
const bitgo = new BitGo({ env: environment });
bitgo.authenticateWithAccessToken({ accessToken });
if (otp) {
const unlock = await bitgo.unlock({ otp, duration: 3600 });
if (!unlock) {
console.log('We did not unlock.');
throw new Error();
}
}

const baseCoin = bitgo.coin(coin);

console.log(`Environment: ${environment}`);
console.log(`Coin: ${coin}`);
console.log(`Wallet ID: ${walletId}`);

// Read messages from JSON file
const messagesPath = path.join(__dirname, 'messages.json');
const messagesContent = fs.readFileSync(messagesPath, 'utf-8');
const messages: MessageEntry[] = JSON.parse(messagesContent);

if (!Array.isArray(messages) || messages.length === 0) {
throw new Error('messages.json must contain an array of {address, message} objects');
}

console.log(`\nLoaded ${messages.length} message(s) to prove:`);
messages.forEach((m, i) => {
console.log(` ${i + 1}. Address: ${m.address}`);
console.log(` Message: ${m.message}`);
});

// Get the wallet
console.log('\nFetching wallet...');
const wallet = await baseCoin.wallets().get({ id: walletId });
console.log(`Wallet label: ${wallet.label()}`);

// Get keychains for the wallet (needed for deriving pubkeys)
console.log('\nFetching keychains...');
const keychains = await baseCoin.keychains().getKeysForSigning({ wallet });
const xpubs = keychains.map((k) => {
if (!k.pub) {
throw new Error('Keychain missing public key');
}
return k.pub;
});
console.log('Retrieved wallet public keys');

// Create RootWalletKeys from xpubs for derivation
const bip32 = BIP32Factory(ecc);
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(
xpubs.map((xpub) => bip32.fromBase58(xpub)) as utxolib.bitgo.Triple<utxolib.BIP32Interface>
);

// Build messageInfo array by getting address details for each message
console.log('\nBuilding message info from address data...');
const messageInfo: bip322.MessageInfo[] = [];

for (const entry of messages) {
// Get address information from wallet to obtain chain and index
console.log(` Getting address info for: ${entry.address}`);
const addressInfo = await wallet.getAddress({ address: entry.address });

if (addressInfo.chain === undefined || addressInfo.index === undefined) {
throw new Error(`Address ${entry.address} is missing chain or index information`);
}

const chain = addressInfo.chain as utxolib.bitgo.ChainCode;
const index = addressInfo.index;

// Derive scriptType from chain
const scriptType = utxolib.bitgo.scriptTypeForChain(chain);

// Derive pubkeys for this address using chain and index
const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chain, index);
const pubkeys = derivedKeys.publicKeys.map((pk) => pk.toString('hex'));

console.log(` Chain: ${chain}, Index: ${index}, ScriptType: ${scriptType}`);

messageInfo.push({
address: entry.address,
message: entry.message,
pubkeys,
scriptType,
});
}

console.log('\nCreating BIP322 proof via sendMany...');
const sendManyResult = await wallet.sendMany({
recipients: [],
messages: messages,
walletPassphrase,
});

console.log('BIP322 proof created successfully');

// Extract the signed transaction from the result
// The result should contain the fully signed PSBT or transaction hex
const txHex = sendManyResult.txHex || sendManyResult.tx;
if (!txHex) {
throw new Error('No transaction hex found in sendMany result');
}

console.log('\nVerifying BIP322 proof...');

// Parse the transaction and verify
const network = (baseCoin as AbstractUtxoCoin).network;

// Check if it's a PSBT or raw transaction
if (utxolib.bitgo.isPsbt(txHex)) {
// Parse as PSBT
const psbt = utxolib.bitgo.createPsbtFromHex(txHex, network);
bip322.assertBip322PsbtProof(psbt, messageInfo);
console.log('PSBT proof verified successfully!');
} else {
// Parse as raw transaction
const tx = utxolib.bitgo.createTransactionFromHex<bigint>(txHex, network, { amountType: 'bigint' });
bip322.assertBip322TxProof(tx, messageInfo);
console.log('Transaction proof verified successfully!');
}

// Display summary
console.log('\n============================================');
console.log('BIP322 PROOF VERIFICATION COMPLETE');
console.log('============================================');
console.log(`Verified ${messageInfo.length} address/message pair(s):`);
messageInfo.forEach((info, i) => {
console.log(`\n${i + 1}. Address: ${info.address}`);
console.log(` Message: "${info.message}"`);
console.log(` Script Type: ${info.scriptType}`);
});
console.log('\nAll proofs are valid. The wallet controls the specified addresses.');
}

// Run the example
main()
.then(() => {
console.log('\nExample completed successfully.');
process.exit(0);
})
.catch((e) => {
console.error('\nExample failed with error:', e.message);
if (e.result) {
console.error('API Error details:', JSON.stringify(e.result, null, 2));
}
process.exit(1);
});