Testing & Debugging Bitcoin Applications
Testing Bitcoin applications requires special considerations due to the financial nature of the software and the complexity of the protocol. This guide covers testing strategies, debugging techniques, and best practices.
Testing Networks
Regtest (Recommended for Development)
Regtest provides a completely controlled environment where you can instantly generate blocks.
Setup:
# Start Bitcoin Core in regtest mode
bitcoind -regtest -daemon
# Create a wallet
bitcoin-cli -regtest createwallet "test"
# Generate initial blocks (need 100+ for spendable coins)
bitcoin-cli -regtest -generate 101
Advantages:
- Instant block generation
- No network dependencies
- Complete control over timing
- Reproducible tests
Signet (Recommended for Integration Testing)
Signet provides a more realistic testing environment with predictable block times.
# Start Bitcoin Core in signet mode
bitcoind -signet -daemon
# Check sync status
bitcoin-cli -signet getblockchaininfo
Testnet (Legacy Testing)
Testnet mimics mainnet but with worthless coins.
bitcoind -testnet -daemon
bitcoin-cli -testnet getblockchaininfo
Unit Testing Strategies
Testing Address Generation
Testing Transaction Construction
Integration Testing
Testing with Regtest
Complete Test Flow:
import subprocess
import time
import json
import unittest
class RegtestTestCase(unittest.TestCase):
"""Base test case for Bitcoin regtest integration tests."""
def setUp(self):
# Start bitcoind in regtest mode
subprocess.run(['bitcoind', '-regtest', '-daemon'])
time.sleep(2) # Wait for startup
# Create wallet and generate coins
self.cli('createwallet', 'test')
self.cli('-generate', '101')
def tearDown(self):
# Stop bitcoind
self.cli('stop')
def cli(self, *args):
"""Execute bitcoin-cli command and return parsed result."""
result = subprocess.run(
['bitcoin-cli', '-regtest'] + list(args),
capture_output=True,
text=True
)
if result.stdout:
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
return result.stdout.strip()
return None
def test_send_transaction(self):
# Get a new address
addr = self.cli('getnewaddress')
# Send coins
txid = self.cli('sendtoaddress', addr, '1.0')
# Verify transaction exists
tx = self.cli('gettransaction', txid)
self.assertIsNotNone(tx)
self.assertEqual(tx['amount'], 1.0)
Testing Fee Estimation
def test_fee_estimation(self):
# Generate some blocks with transactions
for _ in range(10):
addr = self.cli('getnewaddress')
self.cli('sendtoaddress', addr, '0.1')
self.cli('-generate', '1')
# Test fee estimation
fee_rate = self.cli('estimatesmartfee', '6')
# Should return a valid fee rate
assert 'feerate' in fee_rate
assert fee_rate['feerate'] > 0
Debugging Techniques
Using bitcoin-cli for Debugging
Inspect Transaction:
# Decode raw transaction
bitcoin-cli decoderawtransaction <hex>
# Get transaction details
bitcoin-cli getrawtransaction <txid> true
# Check mempool
bitcoin-cli getmempoolentry <txid>
Debug Scripts:
# Test script execution (if using btcdeb)
btcdeb '[OP_DUP OP_HASH160 <pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG]'
Analyzing Debug Logs
Bitcoin Core writes detailed logs to debug.log.
Location:
- Linux:
~/.bitcoin/debug.log - macOS:
~/Library/Application Support/Bitcoin/debug.log - Windows:
%APPDATA%\Bitcoin\debug.log
Useful Log Categories:
# Enable specific debug categories
bitcoind -debug=net -debug=mempool -debug=validation
# Or in bitcoin.conf
debug=net
debug=mempool
debug=validation
Common Debug Categories:
net: Network messagesmempool: Mempool operationsvalidation: Block validationrpc: RPC callsestimatefee: Fee estimationselectcoins: Coin selection
Common Debugging Patterns
Transaction Not Confirming:
# Check if in mempool
bitcoin-cli getmempoolentry <txid>
# Check mempool info
bitcoin-cli getmempoolinfo
# Check fee rate
bitcoin-cli getmempoolentry <txid> | jq '.fees.base / .vsize'
Script Verification Failed:
# Decode and inspect the transaction
bitcoin-cli decoderawtransaction <raw_tx>
# Check the referenced output
bitcoin-cli gettxout <prev_txid> <vout>
Testing Lightning Applications
Using Polar
Polar provides a one-click Lightning Network for testing.
Setup:
- Download from lightningpolar.com
- Create a new network
- Start nodes
- Open channels between nodes
LND Testing
import grpc
import lnrpc
import unittest
class LightningTestCase(unittest.TestCase):
"""Test case for LND Lightning Network integration tests."""
def setUp(self):
# Connect to LND
self.channel = grpc.insecure_channel('localhost:10009')
self.stub = lnrpc.LightningStub(self.channel)
def tearDown(self):
self.channel.close()
def test_create_invoice(self):
# Create invoice
request = lnrpc.Invoice(value=1000, memo="test")
response = self.stub.AddInvoice(request)
# Verify invoice created
self.assertIsNotNone(response.payment_request)
self.assertTrue(response.payment_request.startswith('ln'))
Core Lightning Testing
from pyln.client import LightningRpc
import unittest
class CLightningTestCase(unittest.TestCase):
"""Test case for Core Lightning integration tests."""
def setUp(self):
self.rpc = LightningRpc("/path/to/lightning-rpc")
def test_get_info(self):
info = self.rpc.getinfo()
self.assertIn('id', info)
self.assertIn('alias', info)
Mocking and Stubbing
Mocking RPC Calls
from unittest.mock import Mock, patch
class TestWithMocks:
@patch('bitcoinrpc.RawProxy')
def test_get_balance(self, mock_proxy):
# Setup mock
mock_proxy.return_value.getbalance.return_value = 10.5
# Test code that uses RPC
rpc = mock_proxy()
balance = rpc.getbalance()
assert balance == 10.5
mock_proxy.return_value.getbalance.assert_called_once()
Mocking Network Responses
import responses
import requests
class TestAPIIntegration:
@responses.activate
def test_fetch_block(self):
# Mock the API response
responses.add(
responses.GET,
'https://blockstream.info/api/block/000000...',
json={'height': 100000, 'tx_count': 50},
status=200
)
# Test code
response = requests.get('https://blockstream.info/api/block/000000...')
data = response.json()
assert data['height'] == 100000
Test Data Generation
Creating Test Transactions
def create_test_transaction(inputs, outputs):
"""Create a transaction for testing."""
tx = CTransaction()
for txid, vout in inputs:
outpoint = COutPoint(bytes.fromhex(txid)[::-1], vout)
tx.vin.append(CTxIn(outpoint))
for address, amount in outputs:
script = address.to_scriptPubKey()
tx.vout.append(CTxOut(int(amount * 100000000), script))
return tx
Generating Test Keys
import secrets
from bitcoin.wallet import CBitcoinSecret
def generate_test_keypair():
"""Generate a random keypair for testing."""
# Generate random private key
private_key_bytes = secrets.token_bytes(32)
private_key = CBitcoinSecret.from_secret_bytes(private_key_bytes)
# Derive public key
public_key = private_key.pub
return private_key, public_key
Continuous Integration
GitHub Actions Example
name: Bitcoin Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install python-bitcoinlib pytest
- name: Install Bitcoin Core
run: |
wget https://bitcoincore.org/bin/bitcoin-core-25.0/bitcoin-25.0-x86_64-linux-gnu.tar.gz
tar xzf bitcoin-25.0-x86_64-linux-gnu.tar.gz
sudo mv bitcoin-25.0/bin/* /usr/local/bin/
- name: Run unit tests
run: pytest tests/unit/
- name: Run integration tests
run: |
bitcoind -regtest -daemon
sleep 5
pytest tests/integration/
bitcoin-cli -regtest stop
Best Practices
Testing Checklist
- Unit Tests: Test individual functions in isolation
- Integration Tests: Test component interactions on regtest
- End-to-End Tests: Test full workflows on signet/testnet
- Edge Cases: Test boundary conditions and error handling
- Security Tests: Test for common vulnerabilities
Common Pitfalls
Don't:
- Test with real Bitcoin (mainnet)
- Hardcode test data that could change
- Skip error handling tests
- Ignore race conditions in async code
Do:
- Use regtest for fast iteration
- Test both success and failure paths
- Mock external dependencies
- Clean up test state between runs
Test Organization
tests/
├── unit/
│ ├── test_transactions.py
│ ├── test_addresses.py
│ └── test_scripts.py
├── integration/
│ ├── test_wallet.py
│ ├── test_mempool.py
│ └── test_mining.py
├── fixtures/
│ ├── transactions.json
│ └── blocks.json
└── conftest.py
Related Topics
- Test Networks - Regtest, Signet, and Testnet setup
- Getting Started - Development environment setup
- Libraries & SDKs - Testing utilities in each library
