diff --git a/examples/ts/btc/bip322/README.md b/examples/ts/btc/bip322/README.md new file mode 100644 index 0000000000..3a9e884e74 --- /dev/null +++ b/examples/ts/btc/bip322/README.md @@ -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) diff --git a/examples/ts/btc/bip322/messages.json b/examples/ts/btc/bip322/messages.json new file mode 100644 index 0000000000..33ac9871f5 --- /dev/null +++ b/examples/ts/btc/bip322/messages.json @@ -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" + } +] diff --git a/examples/ts/btc/bip322/verifyProof.ts b/examples/ts/btc/bip322/verifyProof.ts new file mode 100644 index 0000000000..de3bd79208 --- /dev/null +++ b/examples/ts/btc/bip322/verifyProof.ts @@ -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 { + // 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 + ); + + // 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(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); + });