B++ Logo

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 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 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 messages
  • mempool: Mempool operations
  • validation: Block validation
  • rpc: RPC calls
  • estimatefee: Fee estimation
  • selectcoins: 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:

  1. Download from lightningpolar.com
  2. Create a new network
  3. Start nodes
  4. 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

  1. Unit Tests: Test individual functions in isolation
  2. Integration Tests: Test component interactions on regtest
  3. End-to-End Tests: Test full workflows on signet/testnet
  4. Edge Cases: Test boundary conditions and error handling
  5. 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