This document explains how to independently calculate and verify Bitcoin deposit addresses for the CBTC bridge system, enabling trustless verification of BTC reserves.
- Overview
- Quick Start
- Address Derivation Algorithm
- Implementation Guide
- API Reference
- Security Considerations
The CBTC bridge uses Taproot (P2TR) addresses with script-path spending. Each deposit account has a unique address derived from:
- A deposit account ID (from Canton blockchain)
- An aggregated threshold public key (x-only pubkey)
- A deterministic unspendable key (derived from the deposit ID)
This design enables threshold signature verification while ensuring each deposit account has a unique, deterministic address.
Traditional "Proof of Reserve" systems require trusting the bridge operator to provide accurate address lists. This implementation enables zero-trust verification:
- π Trustless: Third parties (like Chainlink) can independently calculate all addresses
- π Verifiable: All addresses are derived from the threshold pubkey, not provided by the operator
- π« Censorship-resistant: Operators cannot hide or exclude addresses
- β Complete: Every deposit account must have a corresponding address
git clone https://github.com/YOUR_USERNAME/cbtc-por-tools.git
cd cbtc-por-tools
npm install# Calculate against local attestor
npm run calculate:local
# Calculate against testnet attestor
npm run calculate:testnet
# Or specify a custom attestor URL
npm run calculate http://your-attestor-url:8080The calculation script:
- β Fetches deposit account data from the attestor API
- β Independently calculates Bitcoin addresses using the threshold pubkey and deposit IDs
- β Verifies calculated addresses match what the attestor reports
- β Queries the Bitcoin blockchain (via Esplora) for UTXOs at each address
- β Sums all UTXO values to calculate total BTC in reserve
Query the attestor API endpoint:
GET /app/get-address-calculation-dataResponse format:
{
"chains": [
{
"chain": "devnet",
"xpub": "tpubD6NzVbkrYhZ4...",
"addresses": [
{
"id": "00f8d227b43f5e0af1cd0cde4c2f49f6eacff1bf0f3eea19f42f1a40f60dc4ba4c",
"address_for_verification": "tb1p..."
}
]
}
],
"bitcoin_network": "testnet"
}Key Points:
- The
xpubis shared across all addresses in a chain - Different Canton chains may use different xpubs (different signer groups)
- The xpub is already derived to m/0/0 by the attestor - no additional derivation needed
The xpub field contains the threshold signature group's extended public key already derived to path m/0/0. The attestor performs this derivation before returning the xpub. This is a BIP32 extended public key in Base58 format.
Parse the xpub to extract:
- The public key bytes (33 bytes compressed)
- The chain code (not used for address calculation, but part of the xpub structure)
Important: Do NOT derive the xpub further. The attestor has already derived it to m/0/0, which is the key used for all deposit addresses on this chain.
Taproot uses x-only public keys (32 bytes, x-coordinate only). Strip the prefix byte from the 33-byte compressed public key to get the x-only pubkey.
const xOnlyPubkey = xpubDecoded.publicKey.slice(1); // Remove first byteBuild the Taproot script that will be used for script-path spending:
<x_only_pubkey> OP_CHECKSIG
This script requires a signature from the threshold group to spend funds.
The internal key (unspendable key) is derived deterministically from the deposit account ID:
-
Hash the ID: SHA-256 hash the deposit account ID string to get 32 bytes
-
Create Extended Public Key:
- Public Key: Fixed unspendable point
0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 - Chain Code: Use the hashed ID as the chain code
- Parent Fingerprint:
00000000 - Depth: 3
- Child Number: First hardened child (0x80000000)
- Network: Match the Bitcoin network (mainnet/testnet)
- Public Key: Fixed unspendable point
-
Derive m/0/0: Derive the extended public key at path m/0/0 (non-hardened) using BIP32 derivation
-
Extract X-Only Key: Convert the derived public key to x-only format
Use the Taproot construction:
- Create Taproot Tree: Build a Merkle tree with the script from Step 4
- Tweak the Internal Key: Apply the Taproot tweak to the unspendable key:
tweaked_key = internal_key + tagged_hash("TapTweak", internal_key || merkle_root) * G - Create P2TR Output: The output script is:
OP_1 <32-byte tweaked x-only pubkey>
Encode the P2TR output script as a Bech32m address with the appropriate network prefix:
- Mainnet:
bc1p... - Testnet:
tb1p... - Regtest:
bcrt1p...
const UNSPENDABLE_PUBLIC_KEY = '0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0';
const PARENT_FINGERPRINT = '00000000';
const DEPTH = 3;- TypeScript/JavaScript:
bitcoinjs-lib,bip32,tiny-secp256k1 - Python:
python-bitcoinlib,bip32 - Rust:
bitcoin,bip32,secp256k1
Input:
id = "00f8d227b..."
xpub = "tpub..."
network = "testnet"
Step 1: Parse xpub β Extract public key (33 bytes) and chain code
Step 2: Convert to x-only pubkey (32 bytes)
Step 3: Build script: <x_only_pubkey> OP_CHECKSIG
Step 4: Hash ID with SHA-256
Step 5: Create unspendable extended key with hashed ID as chain code
Step 6: Derive unspendable key at m/0/0
Step 7: Build Taproot tree with script
Step 8: Tweak internal key with Merkle root
Step 9: Create P2TR output script
Step 10: Encode as Bech32m address
Output: tb1p... (Taproot address)
See calculate-bitcoin-addresses.ts for a complete working implementation.
Returns the current total CBTC supply and last update timestamp.
Endpoint:
http://ATTESTOR_URL/app/get-total-cbtc-supply
Response:
{
"status": "ready",
"total_supply_cbtc": "7.899823260000001",
"last_updated": "2025-12-19T17:04:07.328982+00:00"
}Fields:
status: System status ("ready" indicates the attestor is operational)total_supply_cbtc: Total BTC supply backing CBTC (string representation of decimal value)last_updated: ISO 8601 timestamp of when the supply was last calculated
Returns all data needed to independently calculate Bitcoin addresses, grouped by Canton chain, with the threshold xpub for each chain.
Endpoint:
http://ATTESTOR_URL/app/get-address-calculation-data
Purpose: This endpoint provides the raw data (deposit IDs and xpubs) needed for third parties to independently calculate and verify all Bitcoin addresses. The addresses in the response should NOT be trusted - they are provided only for verification purposes.
Response:
{
"chains": [
{
"chain": "devnet",
"xpub": "tpubD6NzVbkrYhZ4...",
"addresses": [
{
"id": "00f8d227b...",
"address_for_verification": "tb1p..."
}
]
}
],
"bitcoin_network": "testnet"
}Fields:
chains: Array of chain groups (one per Canton network)chain: Canton network name (e.g., "devnet", "mainnet")xpub: BIP32 extended public key for this signer group (already derived to m/0/0)addresses: Array of deposit accounts on this chainid: Deposit account ID (hex string)address_for_verification: Bitcoin address provided by attestor (DO NOT TRUST - must be independently calculated and verified!)
bitcoin_network: Bitcoin network ("mainnet", "testnet", "regtest")
Important: The script does not trust the address_for_verification field. It independently calculates each address and verifies it matches. This prevents the attestor from providing incorrect addresses.
To verify the bridge's Bitcoin holdings:
- Fetch all deposit data from
/get-address-calculation-data - For each chain, use the already-derived
xpubto independently calculate addresses - Verify calculated addresses match the ones provided by the attestor
- Query Bitcoin blockchain (via Esplora, Electrum, or Bitcoin Core) for UTXOs at each address
- Sum all UTXO values to get total BTC in the bridge
This ensures:
- β Addresses are correctly derived from the threshold pubkey
- β No addresses can be excluded or added by the attestor
- β UTXO data comes from the Bitcoin blockchain, not the attestor
- β Complete trustless verification of proof of reserves
================================================================================
Bitcoin Address Calculation and Proof of Reserve
================================================================================
Network: testnet
Total deposit accounts: 42
Chain: devnet (xpub: tpubD6NzVbkrYhZ4...)
β
tb1p3xk2...: 2 UTXOs, 0.5 BTC
β
tb1p7ym9...: 1 UTXO, 1.2 BTC
β
tb1pqrs4...: 3 UTXOs, 0.8 BTC
================================================================================
Summary
================================================================================
β
Verified addresses: 42/42
Total UTXOs: 156
Total BTC in Reserve: 12.34567890 BTC
================================================================================
- bitcoinjs-lib: Bitcoin protocol implementation
- bip32: BIP32 extended key derivation
- tiny-secp256k1: Elliptic curve cryptography for Bitcoin
Install with:
npm install bitcoinjs-lib bip32 tiny-secp256k1 @types/node ts-node typescriptApache-2.0