Exploit BIP-94's 20-minute difficulty exception to mine testnet4 blocks with nothing but a CPU.
Testnet4 (BIP-94) has a 20-minute difficulty exception: when a block's timestamp exceeds the previous block's timestamp by more than 1200 seconds, difficulty resets to 1 — making CPU mining viable.
Casini exploits this by crafting blocks with future timestamps (up to the consensus limit of now + 7200s) that trigger the difficulty-1 exception, then grinding SHA256d nonces at ~5 MH/s until a valid proof-of-work is found.
Consensus Window
◄───────────────────────────────────────────►
│ │
prev_time prev_time + 1201 now + 7200
│ │ │
▼ ▼ ▼
─────┼────────────────────┼──────────────────────┼─────► time
│ normal diff │ difficulty = 1 │
│ │ (BIP-94 rule) │
│ │ ▲ │
│ │ │ │
│ │ block timestamp │
│ │ set here │
│ │ │
│ ├───── mine window ─────┤
│ │ │
│◄─── wait then ───────►│
│ submit when │
│ ts < now + 7200 │
Rather than mining one block and waiting idle, Casini builds chains of blocks during the wait period:
Template (mempool txs) Coinbase-only
┌──────────────┐ ┌──────────────┐
│ Block 0 │──parent──│ Block 1 │
│ height N │ hash │ height N+1 │
│ 228 txs │ │ 1 tx │
│ ts=T │ │ ts=T+1201 │
└──────────────┘ └──────────────┘
│ │
grind nonce grind nonce
during wait during wait
│ │
▼ ▼
submit when submit when
T < now+7200 T+1201 < now+7200
Block 0 is built from getblocktemplate and includes mempool transactions (higher reward). Child blocks are coinbase-only, each with ts = parent_ts + 1201 to satisfy the BIP-94 rule. All nonces are ground in parallel while waiting for the timestamp windows to open, then blocks are submitted sequentially as each window arrives.
┌─────────────────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ bitcoin-node │ │ miner │ │
│ │ │ │ │ │
│ │ Bitcoin Core v30.2 │◄──RPC───│ mine.sh (orchestrator) │ │
│ │ Testnet4 full node │ :48332 │ │ │ │
│ │ │ │ ▼ │ │
│ │ ┌────────────────┐ │ │ ┌──────────────────┐ │ │
│ │ │ Wallet: mining │ │ │ │ cpu_miner.py │ │ │
│ │ │ (auto-created) │ │ │ │ Chain builder │ │ │
│ │ └────────────────┘ │ │ │ Block assembler │ │ │
│ │ │ │ │ Submit logic │ │ │
│ │ Ports: │ │ └───────┬──────────┘ │ │
│ │ 48332 RPC (local) │ │ │ │ │
│ │ 48333 P2P (public) │ │ ▼ │ │
│ │ 28332 ZMQ blocks │ │ ┌──────────────────┐ │ │
│ │ 28333 ZMQ txs │ │ │ nonce_grinder │ │ │
│ └─────────────────────┘ │ │ 8-thread C │ │ │
│ │ │ SHA256d scanner │ │ │
│ │ │ ~5.36 MH/s │ │ │
│ │ │ midstate optim. │ │ │
│ │ └──────────────────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Get Block │ │ Build Block │ │ Grind Nonce │ │ Wait for │ │ Submit │
│ Template │───►│ Header + │───►│ (8 threads, │───►│ Timestamp │───►│ Block │
│ (RPC) │ │ Coinbase TX │ │ midstate) │ │ Window │ │ (RPC) │
└─────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ │
│ stale-tip check │ stale-tip check
│ every 5s │ every 5s
▼ ▼
abort if chain abort if chain
outpaced outpaced
Here's what a typical mining session looks like:
[2026-03-27 16:00:19] ============================================
[2026-03-27 16:00:19] Casini — Testnet4 CPU Miner
[2026-03-27 16:00:19] Target: 500 BTC
[2026-03-27 16:00:19] ============================================
[2026-03-27 16:00:19] Waiting for bitcoin node RPC to become available...
[2026-03-27 16:00:19] Node RPC is available.
[2026-03-27 16:00:19] Initial block download complete.
[2026-03-27 16:00:19] Setting up mining wallet 'mining'...
[2026-03-27 16:00:19] Reusing mining address: tb1pqk0mq7jh6y5pfzfq460ufl6gtdeaankwcpc5cyrewc564qfjjc0s9qvzzy
[2026-03-27 16:00:19] --- Status ---
[2026-03-27 16:00:19] Block height: 127539
[2026-03-27 16:00:19] Blocks mined: 0
[2026-03-27 16:00:19] Spendable balance: 0.00628087 BTC
[2026-03-27 16:00:19] Immature balance: 350.00888976 BTC
[2026-03-27 16:00:19] Target: 500 BTC
[2026-03-27 16:00:19] Mining 1 block(s)...
[2026-03-27 16:00:19] === Mining round 1 (mined 0/1) ===
[2026-03-27 16:00:19] Chain mode: first submit in 698s (height 127540)
[2026-03-27 16:00:19] [0] Height 127540, 228 txs, 50.00435893 BTC, ts=1774635118
Grinding with 8 threads, midstate optimization...
T0: 10M nonces
T3: 10M nonces
...
[2026-03-27 16:02:44] [0] Found nonce: 82941023
[2026-03-27 16:02:44] [1] Height 127541, 1 tx, 50.00000000 BTC, ts=1774636319
Grinding with 8 threads, midstate optimization...
[2026-03-27 16:05:11] [1] Found nonce: 194820437
[2026-03-27 16:05:11] Chain ready: 2 block(s), heights 127540-127541
[2026-03-27 16:11:38] [0] Submitting 00000000a1b2c3d4... (height 127540)
[2026-03-27 16:11:38] *** Block ACCEPTED! Hash: 00000000a1b2c3d4e5f6... ***
[2026-03-27 16:31:39] [1] Submitting 00000000f9e8d7c6... (height 127541)
[2026-03-27 16:31:39] *** Block ACCEPTED! Hash: 00000000f9e8d7c6b5a4... ***
- Docker & Docker Compose
- 2+ CPU cores (8 recommended)
- 4 GB RAM minimum
- 10 GB disk space
git clone https://github.com/recorner/casini.git
cd casini
# Generate RPC credentials
python3 scripts/gen-rpcauth.py
# Paste rpcauth= line into bitcoin-node/bitcoin.conf
# Copy credentials into .env
cp .env.example .env
# Edit .env with the generated passwordNever commit
.env— it is gitignored by default.
docker compose build bitcoin-node # ~15-20 min (compiles Bitcoin Core from source)
docker compose up -d bitcoin-nodechmod +x scripts/*.sh
./scripts/wait-for-sync.sh
# Or check manually
docker exec casini-bitcoin-node bitcoin-cli -datadir=/home/bitcoin/.bitcoin getblockchaininfodocker compose build miner
docker compose up -d miner
# Watch mining progress
docker compose logs -f minerThe miner automatically:
- Creates a
miningwallet (Taproot/bech32m address) - Detects whether
generatetoaddressor the customcpu_minershould be used - Builds chains of blocks to maximize throughput
- Tracks orphaned blocks and reports live balance
./scripts/check-balance.shA multi-threaded C program that performs the proof-of-work search:
- Midstate optimization: Pre-computes SHA256 state for the first 64 bytes of the header, only processes the final 16 bytes + nonce per iteration
- 8 parallel threads scanning non-overlapping nonce ranges (0 to 2³²)
- ~670 KH/s per thread, ~5.36 MH/s total on an 8-core Xeon
- Finds a difficulty-1 nonce in 2-13 minutes on average
- Compiled with
gcc -O3 -march=native
Header (80 bytes)
├─ bytes 0-63 ──► SHA256 midstate (precomputed once)
└─ bytes 64-79 ──► last 16 bytes iterated with nonce
│
├─ Thread 0: nonce 0x00000000 – 0x1FFFFFFF
├─ Thread 1: nonce 0x20000000 – 0x3FFFFFFF
├─ ...
└─ Thread 7: nonce 0xE0000000 – 0xFFFFFFFF
Python script that orchestrates the full mining pipeline:
getblocktemplate— fetches mempool transactions and block parameters- Build coinbase TX — segwit coinbase with witness commitment
- Construct block header — version, prev hash, merkle root, timestamp, nBits
- Grind nonce — calls
nonce_grinderbinary, monitors for stale tips - Build child blocks — coinbase-only blocks using parent hash
- Submit chain — waits for each timestamp window, submits sequentially
Key features:
- Stale-tip detection every 5s during grinding (aborts if chain outpaced)
- Extra-nonce rotation to avoid nonce-space exhaustion
- Configurable
MAX_CHAINdepth (default: 2)
Bash wrapper that manages the mining lifecycle:
- Node readiness checks and IBD (Initial Block Download) monitoring
- Wallet auto-creation and address management
- Smart routing:
generatetoaddresswhen real-time is ahead,cpu_minerwhen timestamps are in the future - Live balance reporting (spendable + immature)
- Orphan detection — checks all mined block hashes for
-1confirmations
| Variable | Default | Description |
|---|---|---|
BITCOIN_RPC_HOST |
bitcoin-node |
Node hostname |
BITCOIN_RPC_PORT |
48332 |
RPC port |
BITCOIN_RPC_USER |
casini |
RPC username |
BITCOIN_RPC_PASSWORD |
(required) | RPC password |
MINING_TARGET_BTC |
500 |
Target BTC balance |
MINING_WALLET |
mining |
Wallet name |
BLOCKS_PER_BATCH |
1 |
Blocks per mining cycle |
| Port | Protocol | Binding | Purpose |
|---|---|---|---|
| 48332 | RPC | 127.0.0.1 |
Bitcoin Core JSON-RPC |
| 48333 | P2P | 0.0.0.0 |
Peer discovery |
| 28332 | ZMQ | 127.0.0.1 |
Raw block notifications |
| 28333 | ZMQ | 127.0.0.1 |
Raw transaction notifications |
Casini includes scripts for wallet/payment system testing after mining:
# Create and fund test wallets (sender, receiver, merchant, customer)
./scripts/setup-wallet.sh
# Run load test: 2 TPS for 5 minutes with auto-mining every 30s
./scripts/generate-load.sh
# Customize parameters
./scripts/generate-load.sh --tps 5 --duration 600 --amount 0.0001 --mine-interval 15Features:
- Round-robin sends between test wallets
- Auto-mines blocks at configurable intervals
- Real-time metrics (actual TPS, mempool depth, failure rate)
- Final summary report
casini/
├── docker-compose.yml # Service orchestration
├── .env.example # Template for RPC credentials
├── bitcoin-node/
│ ├── Dockerfile # Multi-stage Bitcoin Core v30.2 build
│ ├── .dockerignore
│ └── bitcoin.conf # Testnet4 node configuration
├── miner/
│ ├── Dockerfile # Multi-stage build (node-bins → grinder → runtime)
│ ├── .dockerignore
│ ├── mine.sh # Mining orchestrator (bash)
│ ├── cpu_miner.py # Chain mining coordinator (python)
│ └── nonce_grinder.c # Multi-threaded SHA256d scanner (C)
├── scripts/
│ ├── gen-rpcauth.py # Generate RPC credentials
│ ├── wait-for-sync.sh # IBD completion monitor
│ ├── setup-wallet.sh # Create + fund test wallets
│ ├── generate-load.sh # Transaction load generator
│ └── check-balance.sh # Balance reporter
└── README.md
Testnet4 block timestamps run ~6000-7000s ahead of real time. The BIP-94 20-minute rule requires ts > prev_time + 1200, but the MAX_FUTURE_BLOCK_TIME consensus limit caps timestamps at now + 7200. This creates a mandatory wait window: blocks must be ground first, then held until block_time - 7200 < now before submission.
Other miners compete on the same chain. When you submit a block, someone else may have already advanced the tip. Block 0 of each chain has a high acceptance rate (~70-80%); child blocks (block 1, 2+) face increasing orphan rates due to the additional ~1201s wait per child. This is why MAX_CHAIN is set to 2.
Mined block rewards require 100 confirmations before they become spendable. On testnet4 at ~1 block per 20 minutes, that's roughly 33 hours per reward to mature.
docker compose logs bitcoin-nodeNormal behavior — testnet4's 20-minute rule means occasional wait periods when real difficulty is active. The miner handles the transition automatically.
Coinbase rewards require 100 confirmations. Keep mining — each new block matures earlier rewards.
Common in competitive mining. The miner detects orphans automatically and logs warnings. Chain mining reduces orphan risk by submitting connected blocks that resist reorgs.
If the grinder exhausts all 2³² nonces without finding a valid hash (~37% chance at difficulty 1), cpu_miner.py rotates the extra_nonce in the coinbase and retries with a new merkle root.
MIT