Coinbase Rotation for SV2 Mining Pools

Deterministic address derivation for quantum-resistant payout hygiene

Testnet4 Validation#

On January 28, 2026, we validated coinbase rotation on Bitcoin testnet4: two blocks mined back to back, each paying its coinbase to a fresh address derived from a single extended public key—an address whose public key had never been exposed on-chain.

Since then the descriptor has kept producing. Scanning every address the wildcard descriptor can derive gives the full tally below.

Total Blocks Found#

  • 2,042 coinbase outputs paid to addresses derived from this one descriptor
  • 31 distinct derivation indices used, highest index 50
  • Block heights 91,282 – 150,815 (2025-07-15 to 2026-09-03 UTC)

Counts are coinbase outputs paying a derived address, from a full paginated scan of mempool.space testnet4 on 2026-09-03, walking indices 0–80 (gap limit 30).

Rotation was not enabled for this whole span, which is why the blocks are not spread one-per-index. Most of the time the pool ran with rotation off, pinned to a single derivation index, so every block in that stretch landed on the same address:

  • Rotation on (2026-01-28 → 2026-02-02): 28 blocks across 25 distinct indices, the index climbing 0 → 48 as blocks were found. Heights 120,436–121,241. Nearly one fresh address per block, which is the behaviour the feature is meant to produce.
  • Rotation off (everything else): 2,014 blocks across 11 pinned indices — the large clusters in the table below, e.g. 1,413 blocks all paid to index 9 and 223 to index 2. Same descriptor, no rotation, so the public key at those indices is reused exactly the way a static coinbase address would be.

The two runs overlap slightly: a separate non-rotating instance mining to index 9 found 4 blocks during the rotation window.

2026-01-28T17:44:24.925747Z  INFO pool_sv2::channel_manager::mining_message_handler:
    SubmitSharesExtended: 💰 Block Found!!! 💰
    0000000000000000b2d658e434cb1383eb41853a1b8bb23df1993a6c9036f2f7

2026-01-28T19:10:00.262347Z  INFO pool_sv2::channel_manager::mining_message_handler:
    SubmitSharesExtended: 💰 Block Found!!! 💰
    00000000000000005a65b2d963bc3117c3b0f92d5674483e7ab6af1e03d51267

The First Two Rotated Blocks#

Block Height Timestamp (UTC) Derivation Index Address Explorer
1 120535 2026-01-28 17:44:24 4 tb1q5u4w9hwuepxfng9tusl5l0h353k32wuwv56s5x View Block →
2 120549 2026-01-28 19:10:00 5 tb1qlqym3pzn76qz5ecj7y36rwrgxrr83pnwxtc5rp View Block →

Blocks per Derivation Index#

Index Address Blocks Height Range
0tb1q0x8m22780w5wq2xl45d4yfg9yg78ul9qcgs54f2120436–120443
2tb1qm4x6w7t8kz5vt8hrqf40tqk7f94t45fy88fzal22391282–94173
3tb1qqxu95exrervdear0fy3smyuwcyj9lnvwqhc2cv2120460–120741
4tb1q5u4w9hwuepxfng9tusl5l0h353k32wuwv56s5x2194197–120535
5tb1qlqym3pzn76qz5ecj7y36rwrgxrr83pnwxtc5rp19094390–120549
6tb1q3w73mj4yqjnje39dwyhua5jvkkcmrcjdqtq83p3995677–120770
7tb1qfh6d64gqyqhkzew6a5vpwzc22pdx8aln9cselq6295879–96385
8tb1q2zc2k3uh6ketqd6yswzdy8evrcvd8yxn4sw0f520110551–120789
9tb1qvsf5qfckvdq5gm37jkdve9qwp7crmg45dxknkv1413113726–150815
10tb1q0vape6v67xqp3gtkjr6cdpynj6myadtm3nnr2d1113844–113844
11tb1qcqs9g7wm6ggv88xs2aq20jm8tg3hykedkueket2119507–120811
12tb1qqln8aua2yaulg3sp8ynx256exw0fewl29mg75n1120830–120830
13tb1qvjz7mpjlmf2lhtajd5xz52mnxftt69n3d064gk1120844–120844
14tb1qkstw0c7vqlj5f3l89rfekn2vph0gnpvcpakts633141492–150628
15tb1qnl3m2ma7mxx76wvq6fkxkxfy9udc4xur9zjmh31120869–120869
17tb1q780zz8udvevv2hkwwh8yy7mrn7n0c7xyk44ql81120883–120883
21tb1qaaqvgwnmnkavruk0w88nw70j9sglxxghy9vayh1120919–120919
24tb1q074pzqfu3cvezz58dkl3c04x9mharh5xxa0r9n1120955–120955
25tb1qchy6a3awyha5j79v4h6er70y0jtrnuln9c7qsl1120966–120966
34tb1qle0ec0mdz0w20dqdgt7fwq6dqdd7ll5ekyjc271121035–121035
36tb1qw7dwhuhjrus7dsyn9vyn6rn8c33yptm845lxgm1121064–121064
39tb1qmpvugwpdlr85ll9sm58375nherugp82q9d0d0c1121097–121097
40tb1qsfs4nhnxm82dd4x5rhcpqj06kwmwy04jfd6lqw1121124–121124
41tb1qzakh5wp7ve3whgppa9qlwuut0cllw9uz7k9zs21121129–121129
43tb1qaze3cetuzgfyhl5n7zndutqxxue658lxrhm0wu1121162–121162
44tb1qhphtqu7g0nvmg7kjjj69fqk5wrp2w9elpnsv541121193–121193
45tb1qngc93226pk4u5g670df50ayl20xzase59vyh4f1121210–121210
46tb1q7xv06xt6t8slmlf3werzfsx59ty5yu26ssauev1121230–121230
47tb1q3yfwm9fz3txjkm05n8xvf9fe3l8y0f6al4k9c31121239–121239
48tb1q5g6t6n59pvk53q9fu35jga72ldzxepe8ratsvx1121241–121241
50tb1quryp7v2t7wz7dcewjdclv3re9skwt64wd4qsgx16147130–147734

All 2,042 Blocks#

Show all 2,042 blocks (newest first)

Loaded from blocks.json; each row links to the block on mempool.space testnet4. Regenerate the whole list yourself with scan-coinbases.py (stdlib only, no node required): python3 scan-coinbases.py.

Loading…

Descriptor Used#

wpkh(tpubDDHYkDsJ8XB1LLjMNrk5gXsmze87LRkWoNqprdXPud9Yx3ZfsjZZJEqscUgSRLJ1EG77KSKygC9uNAeDtgHsLtvH93MnPF2M9Vq5WvGvcLw/0/*)

The wildcard /* at the end enables automatic rotation. Each block found increments the derivation index, producing a new unique address with an unexposed public key.

The Quantum Threat to Bitcoin#

Bitcoin's security model relies on the Elliptic Curve Digital Signature Algorithm (ECDSA) and the secp256k1 curve. The security assumption is that deriving a private key from a public key is computationally infeasible—a problem known as the Elliptic Curve Discrete Logarithm Problem (ECDLP).

Quantum computers change this calculus. Shor's algorithm, running on a sufficiently powerful quantum computer, can solve ECDLP in polynomial time. While such computers don't exist today, the cryptographic community takes this threat seriously. Bitcoin's long-term security depends on proactive measures.

BIP-360: Pay to Quantum Resistant Hash (P2QRH)#

The Bitcoin community is actively developing quantum-resistant solutions. BIP-360 proposes a new output type using post-quantum cryptographic signatures. Key features include:

  • Lattice-based signatures (e.g., FALCON, SPHINCS+) resistant to Shor's algorithm
  • Backward compatibility via soft fork activation
  • Migration path for existing UTXOs to quantum-safe addresses

Until P2QRH or similar solutions are deployed, minimizing public key exposure is the most effective defense-in-depth measure available today.

Understanding Public Key Exposure#

Bitcoin addresses are derived from public keys through a one-way hash function:

Address = RIPEMD160(SHA256(PublicKey))  // P2PKH
Address = SHA256(PublicKey)[0:20]        // P2WPKH (simplified)

This hash provides a layer of protection: even if an attacker can derive private keys from public keys (the quantum threat), they still cannot derive public keys from addresses (hash preimage resistance). The public key remains hidden until the first spend.

The ECDSA Exposure Window#

flowchart TB subgraph fresh["Fresh Address (never spent from)"] direction TB A["Address = HASH160(pubkey)"] B["Public key hidden behind hash"] C["Quantum-safe: attacker sees only hash"] D["No known attack on hash preimage"] end subgraph exposed["Exposed Address (spent from)"] direction TB E["Transaction reveals full public key"] F["In witness data (SegWit) or scriptSig"] G["Quantum vulnerable: Shor's algorithm applies"] H["Future deposits at risk"] end fresh -->|"First spend transaction"| exposed style fresh fill:#d4edda,stroke:#28a745 style exposed fill:#f8d7da,stroke:#dc3545

Why Address Reuse is Dangerous#

When you spend from an address, the full ECDSA public key is revealed in the transaction's witness data (for SegWit) or scriptSig (for legacy). A quantum attacker could then:

  1. Extract the public key from any historical transaction spending from that address
  2. Run Shor's algorithm to derive the private key
  3. Steal any funds subsequently deposited to that address

This is particularly dangerous for mining pools that reuse a single coinbase address. After the first consolidation transaction, every future block reward sent to that address is quantum-vulnerable from the moment it's mined.

The "Harvest Now, Decrypt Later" Attack#

Nation-state adversaries are already archiving encrypted data and blockchain transactions with the expectation that future quantum computers will enable decryption. This is known as a "harvest now, decrypt later" (HNDL) attack.

For Bitcoin, this means:

Mining Pool Coinbases: A High-Value Target#

Mining pools accumulate significant value in coinbase outputs. A pool using a single static address creates an attractive target: one public key exposure compromises all future deposits. Coinbase rotation eliminates this single point of failure.

Coinbase Rotation: Defense in Depth#

By deriving a fresh address for each block, coinbase rotation ensures:

Quantum Timeline Considerations#

timeline title Quantum Computing & Bitcoin Security Timeline section Current Era 2024-2026 : NISQ devices (noisy, limited qubits) : No threat to ECDSA : Time to implement defensive measures section Near-Term (Speculative) 2030-2035 : Early fault-tolerant systems : Potential threat to weak keys : BIP-360 activation target section Long-Term (Speculative) 2035+ : Cryptographically relevant quantum computers : ECDSA potentially broken : P2QRH protects new UTXOs

Timeline is speculative. Estimates vary widely among cryptographers. The prudent approach is to minimize exposure now rather than react after quantum capabilities emerge.

Additional Benefits#

Beyond quantum resistance, coinbase rotation provides operational benefits:

The Solution: Wildcard Descriptor Rotation#

Coinbase rotation leverages Bitcoin's hierarchical deterministic (HD) wallet standard (BIP-32/44/84) combined with miniscript descriptors. By specifying a wildcard (/*) in the descriptor, the pool automatically derives sequential addresses from a single extended public key—each with a unique, unexposed public key.

Configuration Example#

# pool-config.toml

# Wildcard descriptor enables rotation
coinbase_reward_script = "wpkh(tpub.../0/*)"

# Persistence file (survives restarts)
coinbase_index_file = "/var/lib/pool/coinbase_index.dat"

# Starting derivation index (default: 0)
coinbase_start_index = 0

Supported Descriptor Types#

Type Descriptor Format Address Type
Native SegWit wpkh(xpub.../0/*) bc1q... (P2WPKH)
Taproot tr(xpub.../0/*) bc1p... (P2TR)
Legacy SegWit sh(wpkh(xpub.../0/*)) 3... (P2SH-P2WPKH)

How It Works#

sequenceDiagram participant Miner participant Pool participant Derivator participant Disk Miner->>Pool: Submit valid share (block found!) Pool->>Pool: Validate share meets network difficulty Pool->>Derivator: rotate_coinbase_address() Derivator->>Derivator: index++ (atomic) Derivator->>Derivator: derive(xpub, index) → new scriptPubKey Note over Derivator: Fresh pubkey, never exposed Derivator->>Disk: persist("seq:N") Derivator-->>Pool: new scriptPubKey Pool->>Pool: Update coinbase_outputs Pool-->>Miner: SubmitSharesSuccess

Security Comparison: Static vs. Rotating Addresses#

flowchart LR subgraph static["STATIC ADDRESS"] S1["Block 1"] --> SA["Single Address"] S2["Block 2"] --> SA S3["Block 3"] --> SA S4["Block N"] --> SA SA --> SC["One public key exposure
compromises ALL future deposits
Single point of quantum failure"] end subgraph rotating["ROTATING ADDRESSES"] R1["Block 1"] --> RA1["Address 1"] R2["Block 2"] --> RA2["Address 2"] R3["Block 3"] --> RA3["Address 3"] R4["Block N"] --> RA4["Address N"] RA1 --> RC["Each address independent
Isolated quantum exposure
Defense in depth"] RA2 --> RC RA3 --> RC RA4 --> RC end style static fill:#f8d7da,stroke:#dc3545 style rotating fill:#d4edda,stroke:#28a745

Implementation Details#

The implementation uses Rust's miniscript crate for descriptor parsing and derivation. The current index is stored atomically in memory and persisted to disk after each rotation, ensuring crash-safety and restart resilience.

Key Components#

XpubDerivator
stratum-apps/src/config_helpers/xpub_derivation.rs
Parses wildcard descriptors, derives scriptPubKey at arbitrary indices, thread-safe index management via AtomicU32, persistent storage with format: seq:N
CoinbaseRewardScript
stratum-apps/src/config_helpers/coinbase_output/
Detects wildcard descriptors via has_wildcard() method, returns raw descriptor string for derivator initialization
ChannelManager
pool-apps/pool/src/lib/channel_manager/
Calls rotate_coinbase_address() after block found, updates coinbase_outputs in channel manager data

Persistence Format#

# Index format
seq:42

# Legacy format (treated as sequential)
42

Verify It Yourself#

Don't trust, verify. You can independently confirm that the testnet4 blocks used the correct derived addresses by running the derivation yourself.

Verification Code Snippets

Rust

use miniscript::{Descriptor, descriptor::DescriptorPublicKey, bitcoin::{Network, Address}};

fn main() {
    let descriptor_str = "wpkh(tpubDDHYkDsJ8XB1LLjMNrk5gXsmze87LRkWoNqprdXPud9Yx3ZfsjZZJEqscUgSRLJ1EG77KSKygC9uNAeDtgHsLtvH93MnPF2M9Vq5WvGvcLw/0/*)";
    
    let desc: Descriptor<DescriptorPublicKey> = descriptor_str.parse().unwrap();
    
    // Derive at indices 4 and 5 (the two testnet4 blocks)
    for i in 4..=5 {
        let derived = desc.at_derivation_index(i).unwrap();
        let script = derived.script_pubkey();
        let address = Address::from_script(&script, Network::Testnet).unwrap();
        println!("Index {}: {}", i, address);
    }
}

// Expected output:
// Index 4: tb1q5u4w9hwuepxfng9tusl5l0h353k32wuwv56s5x
// Index 5: tb1qlqym3pzn76qz5ecj7y36rwrgxrr83pnwxtc5rp

Python (with python-bitcoinlib)

from bitcoin.wallet import CBitcoinExtPubKey
from bitcoin.core import Hash160
from bitcoin.bech32 import encode_segwit_address

# tpub from the descriptor (BIP84 account key)
tpub = "tpubDDHYkDsJ8XB1LLjMNrk5gXsmze87LRkWoNqprdXPud9Yx3ZfsjZZJEqscUgSRLJ1EG77KSKygC9uNAeDtgHsLtvH93MnPF2M9Vq5WvGvcLw"

# Parse the extended public key
xpub = CBitcoinExtPubKey(tpub)

# Derive at m/0/4 and m/0/5 (relative to the tpub)
for i in [4, 5]:
    child = xpub.derive(0).derive(i)
    pubkey_hash = Hash160(child.pub.serialize())
    address = encode_segwit_address("tb", 0, pubkey_hash)
    print(f"Index {i}: {address}")

# Expected output:
# Index 4: tb1q5u4w9hwuepxfng9tusl5l0h353k32wuwv56s5x
# Index 5: tb1qlqym3pzn76qz5ecj7y36rwrgxrr83pnwxtc5rp

Bitcoin Core (bitcoin-cli)

# Import the descriptor (watch-only)
bitcoin-cli -testnet4 importdescriptors '[{
  "desc": "wpkh(tpubDDHYkDsJ8XB1LLjMNrk5gXsmze87LRkWoNqprdXPud9Yx3ZfsjZZJEqscUgSRLJ1EG77KSKygC9uNAeDtgHsLtvH93MnPF2M9Vq5WvGvcLw/0/*)#checksum",
  "timestamp": "now",
  "range": [0, 10],
  "watchonly": true
}]'

# Derive addresses at indices 4 and 5
bitcoin-cli -testnet4 deriveaddresses \
  "wpkh(tpubDDHYkDsJ8XB1LLjMNrk5gXsmze87LRkWoNqprdXPud9Yx3ZfsjZZJEqscUgSRLJ1EG77KSKygC9uNAeDtgHsLtvH93MnPF2M9Vq5WvGvcLw/0/*)#checksum" \
  "[4,5]"

# Expected output:
# [
#   "tb1q5u4w9hwuepxfng9tusl5l0h353k32wuwv56s5x",
#   "tb1qlqym3pzn76qz5ecj7y36rwrgxrr83pnwxtc5rp"
# ]

JavaScript (bitcoinjs-lib)

const bitcoin = require('bitcoinjs-lib');
const { BIP32Factory } = require('bip32');
const ecc = require('tiny-secp256k1');

const bip32 = BIP32Factory(ecc);
const network = bitcoin.networks.testnet;

// Parse the tpub
const tpub = "tpubDDHYkDsJ8XB1LLjMNrk5gXsmze87LRkWoNqprdXPud9Yx3ZfsjZZJEqscUgSRLJ1EG77KSKygC9uNAeDtgHsLtvH93MnPF2M9Vq5WvGvcLw";
const node = bip32.fromBase58(tpub, network);

// Derive at m/0/4 and m/0/5
for (const i of [4, 5]) {
    const child = node.derivePath(`0/${i}`);
    const { address } = bitcoin.payments.p2wpkh({ 
        pubkey: child.publicKey, 
        network 
    });
    console.log(`Index ${i}: ${address}`);
}

// Expected output:
// Index 4: tb1q5u4w9hwuepxfng9tusl5l0h353k32wuwv56s5x
// Index 5: tb1qlqym3pzn76qz5ecj7y36rwrgxrr83pnwxtc5rp

Expected Derivation Table#

Index ScriptPubKey (hex) Testnet Address
00014798fb52bc77ba8e028dfad1b522505223c7e7ca0tb1q0x8m22780w5wq2xl45d4yfg9yg78ul9qcgs54f
100143acc8d6d349a24a198fb9eec0e27b822c589d407tb1q8txg6mf5ngj2rx8mnmkqufacytzcn4q8v4yt8n
20014dd4da77967b0a8c59ee3026af582de496abad124tb1qm4x6w7t8kz5vt8hrqf40tqk7f94t45fy88fzal
3001401b85a64c3c8d8dcf46f49230d938ec1245fcd8etb1qqxu95exrervdear0fy3smyuwcyj9lnvwqhc2cv
40014a72ae2dddcc84c99a0abe43f4fbef1a46d153b8etb1q5u4w9hwuepxfng9tusl5l0h353k32wuwv56s5x
50014f809b88453f6802a6712f123a1b86830c678866etb1qlqym3pzn76qz5ecj7y36rwrgxrr83pnwxtc5rp
600148bbd1dcaa404a72cc4ad712fced24cb5b1b1e24dtb1q3w73mj4yqjnje39dwyhua5jvkkcmrcjdqtq83p
700144df4dd5500202f6165daed18170b0a505a63f7f3tb1qfh6d64gqyqhkzew6a5vpwzc22pdx8aln9cselq
8001450b0ab4797d5b2b037448384d21f2c1e18d390d3tb1q2zc2k3uh6ketqd6yswzdy8evrcvd8yxn4sw0f5
9001464134027166341446e3e959acc940e0fb03da2b4tb1qvsf5qfckvdq5gm37jkdve9qwp7crmg45dxknkv

Indices 4 and 5 (highlighted) correspond to the testnet4 blocks mined.

Getting Started#

Clone and Build#

# Clone the feature branch
git clone -b feat/coinbase-rotation \
  https://github.com/average-gary/sv2-apps.git

cd sv2-apps/pool-apps

# Build the pool
cargo build --release -p pool_sv2

Configure Rotation#

# In your pool-config.toml
coinbase_reward_script = "wpkh(YOUR_TPUB_OR_XPUB/0/*)"
coinbase_index_file = "/path/to/coinbase_index.dat"

Descriptor Conversion#

If you have a descriptor from Sparrow or Bitcoin Core in a different format, use:

Further Reading#