Module 3: Best Practices

From Prototype to Production

You can build a Bitcoin application. But can you build one that's secure, reliable, and maintainable? That's what separates hobbyist code from production software.

Bitcoin applications have special requirements—you're dealing with real money, irreversible transactions, and users who expect bank-level security. A single bug can cost thousands of dollars. A poor design choice can leak user privacy. An unhandled edge case can freeze funds.

This module teaches you the discipline and practices that professional Bitcoin developers use to ship reliable, secure software.

Security Considerations

Security isn't an afterthought in Bitcoin development—it's the foundation. Every line of code must be written with the assumption that attackers are looking for vulnerabilities.

Key Management

Critical Security Rules

  • NEVER store private keys or seed phrases in plaintext
  • NEVER log private keys, even in debug mode
  • NEVER send private keys over the network
  • NEVER use weak random number generation for keys
  • NEVER reuse nonces in signatures (use RFC6979)

✓ Best Practice: Secure Key Storage

import os
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2

class SecureKeyStore:
    def __init__(self, password):
        """Initialize secure key storage with password"""
        self.salt = os.urandom(16)

        # Derive encryption key from password
        kdf = PBKDF2(
            algorithm=hashes.SHA256(),
            length=32,
            salt=self.salt,
            iterations=100000,
        )
        key = kdf.derive(password.encode())
        self.cipher = Fernet(base64.urlsafe_b64encode(key))

    def encrypt_key(self, private_key):
        """Encrypt private key for storage"""
        return self.cipher.encrypt(private_key.encode())

    def decrypt_key(self, encrypted_key):
        """Decrypt private key from storage"""
        return self.cipher.decrypt(encrypted_key).decode()

# For production: Use hardware security modules (HSMs)
# or hardware wallets (Trezor, Ledger, ColdCard)

Input Validation

Never trust user input. Validate everything—addresses, amounts, transaction data, all of it.

✓ Best Practice: Comprehensive Input Validation

import re
from bitcoin.core import b58decode_check
from bitcoin.segwit_addr import decode as bech32_decode

class InputValidator:
    @staticmethod
    def validate_address(address, network='mainnet'):
        """Validate Bitcoin address format and network"""
        if not address or not isinstance(address, str):
            raise ValueError("Invalid address format")

        # Check Bech32 (SegWit) addresses
        if address.lower().startswith(('bc1', 'tb1', 'bcrt1')):
            result = bech32_decode(address)
            if result == (None, None):
                raise ValueError("Invalid Bech32 address")

            # Verify network match
            prefix = address[:4].lower()
            expected = {
                'mainnet': 'bc1',
                'testnet': 'tb1',
                'regtest': 'bcrt'
            }.get(network)

            if not address.lower().startswith(expected):
                raise ValueError(f"Address network mismatch")

            return True

        # Check Base58 (legacy) addresses
        try:
            decoded = b58decode_check(address)
            version = decoded[0]

            # Verify version byte matches network
            valid_versions = {
                'mainnet': [0x00, 0x05],  # P2PKH, P2SH
                'testnet': [0x6f, 0xc4],
            }.get(network, [])

            if version not in valid_versions:
                raise ValueError("Invalid address version")

            return True
        except Exception as e:
            raise ValueError(f"Invalid address: {e}")

    @staticmethod
    def validate_amount(amount, max_btc=21_000_000):
        """Validate Bitcoin amount"""
        if not isinstance(amount, (int, float)):
            raise ValueError("Amount must be numeric")

        if amount <= 0:
            raise ValueError("Amount must be positive")

        if amount > max_btc * 100_000_000:  # Convert to sats
            raise ValueError("Amount exceeds maximum possible")

        return True

    @staticmethod
    def validate_txid(txid):
        """Validate transaction ID format"""
        if not isinstance(txid, str):
            raise ValueError("TXID must be string")

        if len(txid) != 64:
            raise ValueError("TXID must be 64 hex characters")

        if not re.match(r'^[0-9a-fA-F]{64}$', txid):
            raise ValueError("TXID contains invalid characters")

        return True

# Use validation in all public functions
def send_transaction(address, amount):
    # Validate inputs first
    InputValidator.validate_address(address)
    InputValidator.validate_amount(amount)

    # Now proceed with transaction
    ...

Transaction Validation

Before signing or broadcasting, validate every aspect of the transaction:

def validate_transaction(tx, utxos):
    """Comprehensive transaction validation"""
    errors = []

    # 1. Check inputs exist and are unspent
    for inp in tx.inputs:
        if inp.outpoint not in utxos:
            errors.append(f"Input {inp.outpoint} not found")

    # 2. Verify input amounts
    total_input = sum(utxos[inp.outpoint].amount for inp in tx.inputs)

    # 3. Verify output amounts
    total_output = sum(out.amount for out in tx.outputs)

    if total_output > total_input:
        errors.append("Outputs exceed inputs")

    # 4. Verify fee is reasonable
    fee = total_input - total_output
    if fee < 0:
        errors.append("Negative fee")
    if fee > total_input * 0.1:  # Fee > 10% of input
        errors.append("Fee suspiciously high")

    # 5. Check for dust outputs
    dust_limit = 546  # satoshis
    for out in tx.outputs:
        if out.amount < dust_limit:
            errors.append(f"Output below dust limit: {out.amount}")

    # 6. Verify signatures (if signed)
    if tx.is_signed():
        for i, inp in enumerate(tx.inputs):
            if not verify_signature(tx, i, utxos[inp.outpoint]):
                errors.append(f"Invalid signature on input {i}")

    # 7. Check for RBF conflicts
    # 8. Verify locktime
    # 9. Check sequence values
    # ... additional checks

    if errors:
        raise ValidationError("Transaction validation failed", errors)

    return True

Testing Strategies

"In production, this will handle real money" should motivate you to test thoroughly. Bitcoin applications require multiple layers of testing.

1. Unit Tests

Test individual functions in isolation:

import pytest
from decimal import Decimal

def test_address_validation():
    """Test address validation logic"""
    validator = InputValidator()

    # Valid addresses
    assert validator.validate_address("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq")
    assert validator.validate_address("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")

    # Invalid addresses
    with pytest.raises(ValueError):
        validator.validate_address("invalid_address")

    with pytest.raises(ValueError):
        validator.validate_address("bc1qinvalidchecksum")

def test_utxo_selection():
    """Test UTXO selection algorithm"""
    utxos = [
        {'amount': 100000, 'txid': 'abc...'},
        {'amount': 50000, 'txid': 'def...'},
        {'amount': 25000, 'txid': 'ghi...'},
    ]

    selector = UTXOSelector(utxos)
    selected, total, fee = selector.select_for_amount(60000, fee_rate=10)

    assert len(selected) == 2
    assert total >= 60000 + fee
    assert fee > 0

def test_fee_calculation():
    """Test fee calculation accuracy"""
    calculator = FeeCalculator()

    # P2WPKH transaction: 2 inputs, 2 outputs
    fee = calculator.calculate(num_inputs=2, num_outputs=2, fee_rate=10)

    # Expected: ~(10 + 68*2 + 31*2) * 10 = ~1980 sats
    assert 1900 < fee < 2100

# Run tests: pytest test_wallet.py -v

2. Integration Tests

Test components working together using regtest:

import pytest
from bitcoin import SelectParams
from bitcoin.rpc import Proxy

@pytest.fixture
def regtest_setup():
    """Setup regtest environment for testing"""
    SelectParams('regtest')
    rpc = Proxy()

    # Create test wallet
    try:
        rpc.createwallet("test_wallet")
    except:
        pass  # Wallet exists

    # Generate blocks for testing
    address = rpc.getnewaddress()
    rpc.generatetoaddress(101, address)

    yield rpc

    # Cleanup
    rpc.unloadwallet("test_wallet")

def test_wallet_transaction_flow(regtest_setup):
    """Test complete transaction flow"""
    rpc = regtest_setup

    # 1. Create two wallets
    wallet1 = SimpleWallet(rpc, "wallet1")
    wallet2 = SimpleWallet(rpc, "wallet2")

    # 2. Fund wallet1
    addr1 = wallet1.get_address()
    rpc.generatetoaddress(1, addr1)

    # 3. Check balance
    balance = wallet1.get_balance()
    assert balance > 0

    # 4. Send to wallet2
    addr2 = wallet2.get_address()
    txid = wallet1.send(addr2, 1.0)
    assert txid is not None

    # 5. Mine block
    rpc.generatetoaddress(1, addr1)

    # 6. Verify wallet2 received
    balance2 = wallet2.get_balance()
    assert balance2 == 1.0

def test_rbf_functionality(regtest_setup):
    """Test Replace-By-Fee"""
    rpc = regtest_setup
    wallet = SimpleWallet(rpc, "test_wallet")

    # Create low-fee transaction
    txid1 = wallet.send(address, 0.5, fee_rate=1)

    # Bump fee
    txid2 = wallet.rbf(txid1, fee_rate=10)

    assert txid1 != txid2
    assert wallet.get_transaction(txid2)['fee'] > wallet.get_transaction(txid1)['fee']

3. Property-Based Testing

Use Hypothesis to test properties that should always hold:

from hypothesis import given, strategies as st

@given(
    num_inputs=st.integers(min_value=1, max_value=100),
    num_outputs=st.integers(min_value=1, max_value=100),
    fee_rate=st.integers(min_value=1, max_value=1000)
)
def test_fee_calculation_properties(num_inputs, num_outputs, fee_rate):
    """Test fee calculation properties"""
    calculator = FeeCalculator()
    fee = calculator.calculate(num_inputs, num_outputs, fee_rate)

    # Property 1: Fee should be positive
    assert fee > 0

    # Property 2: More inputs = higher fee
    fee_more_inputs = calculator.calculate(num_inputs + 1, num_outputs, fee_rate)
    assert fee_more_inputs > fee

    # Property 3: Higher fee rate = higher fee
    fee_higher_rate = calculator.calculate(num_inputs, num_outputs, fee_rate * 2)
    assert fee_higher_rate > fee * 1.9  # Allow for rounding

@given(
    utxos=st.lists(
        st.dictionaries(
            keys=st.just('amount'),
            values=st.integers(min_value=1000, max_value=1000000)
        ),
        min_size=1,
        max_size=20
    ),
    target=st.integers(min_value=1000, max_value=500000)
)
def test_utxo_selection_properties(utxos, target):
    """Test UTXO selection properties"""
    selector = UTXOSelector(utxos)

    try:
        selected, total, fee = selector.select_for_amount(target, fee_rate=10)

        # Property 1: Selected amount covers target + fee
        assert total >= target + fee

        # Property 2: All selected UTXOs are from input set
        assert all(u in utxos for u in selected)

    except ValueError:
        # Insufficient funds is acceptable
        assert sum(u['amount'] for u in utxos) < target

4. Fuzzing

Test with random/malformed inputs to find edge cases:

import atheris
import sys

def test_address_parser(data):
    """Fuzz test address parsing"""
    try:
        # Try parsing random data as address
        validator = InputValidator()
        validator.validate_address(data.decode('utf-8', errors='ignore'))
    except (ValueError, UnicodeDecodeError, Exception):
        # Expected to fail on invalid input
        pass

atheris.Setup(sys.argv, test_address_parser)
atheris.Fuzz()

Error Handling

Bitcoin applications must handle errors gracefully. Network issues, insufficient funds, invalid transactions— all must be handled without losing user funds or corrupting state.

Error Hierarchy

class BitcoinError(Exception):
    """Base class for all Bitcoin-related errors"""
    pass

class NetworkError(BitcoinError):
    """Network communication errors"""
    pass

class ValidationError(BitcoinError):
    """Input validation errors"""
    def __init__(self, message, errors=None):
        super().__init__(message)
        self.errors = errors or []

class InsufficientFundsError(BitcoinError):
    """Not enough funds for transaction"""
    def __init__(self, required, available):
        self.required = required
        self.available = available
        super().__init__(f"Insufficient funds: need {required}, have {available}")

class TransactionError(BitcoinError):
    """Transaction construction or broadcast errors"""
    pass

class SigningError(BitcoinError):
    """Signature creation or verification errors"""
    pass

Defensive Programming

✓ DO

  • ✓ Validate all inputs
  • ✓ Use specific exception types
  • ✓ Log errors with context
  • ✓ Provide helpful error messages
  • ✓ Fail fast on critical errors
  • ✓ Use timeouts for network calls
  • ✓ Implement retry logic
  • ✓ Clean up resources in finally blocks

✗ DON'T

  • ✗ Catch generic Exception
  • ✗ Ignore errors silently
  • ✗ Expose internal errors to users
  • ✗ Continue after critical failure
  • ✗ Log sensitive information
  • ✗ Assume operations will succeed
  • ✗ Use exceptions for control flow
  • ✗ Leave resources uncleaned

Robust Error Handling Example

import logging
from contextlib import contextmanager
import time

logger = logging.getLogger(__name__)

class RobustWallet:
    def send_with_retry(self, address, amount, max_retries=3):
        """Send transaction with automatic retry logic"""
        # Validate inputs first
        try:
            InputValidator.validate_address(address)
            InputValidator.validate_amount(amount)
        except ValueError as e:
            logger.error(f"Invalid input: {e}")
            raise ValidationError(f"Invalid input: {e}")

        # Attempt transaction with retries
        last_error = None
        for attempt in range(max_retries):
            try:
                # Select UTXOs
                utxos = self._select_utxos(amount)

                # Build transaction
                tx = self._build_transaction(utxos, address, amount)

                # Sign transaction
                signed_tx = self._sign_transaction(tx)

                # Broadcast with timeout
                txid = self._broadcast_with_timeout(signed_tx, timeout=30)

                logger.info(f"Transaction broadcast successfully: {txid}")
                return txid

            except InsufficientFundsError as e:
                # Don't retry insufficient funds
                logger.error(f"Insufficient funds: {e}")
                raise

            except NetworkError as e:
                # Retry on network errors
                last_error = e
                logger.warning(f"Network error on attempt {attempt + 1}: {e}")
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)  # Exponential backoff
                    continue

            except TransactionError as e:
                # Don't retry transaction errors
                logger.error(f"Transaction error: {e}")
                raise

            except Exception as e:
                # Log unexpected errors
                logger.exception(f"Unexpected error: {e}")
                raise BitcoinError(f"Unexpected error: {e}")

        # All retries failed
        raise NetworkError(f"Failed after {max_retries} attempts: {last_error}")

    @contextmanager
    def _broadcast_with_timeout(self, tx, timeout=30):
        """Broadcast transaction with timeout"""
        import signal

        def timeout_handler(signum, frame):
            raise TimeoutError("Broadcast timeout")

        # Set timeout
        signal.signal(signal.SIGALRM, timeout_handler)
        signal.alarm(timeout)

        try:
            txid = self.rpc.sendrawtransaction(tx.serialize().hex())
            return txid
        finally:
            # Cancel timeout
            signal.alarm(0)

Code Review Best Practices

Bitcoin code should be reviewed by multiple people. Money is at stake—four eyes are better than two.

What to Look For in Code Review

Security Review Checklist

  • Are private keys ever exposed or logged?
  • Is all user input validated?
  • Are there any SQL injection vectors?
  • Is cryptography used correctly?
  • Are random numbers generated securely?
  • Is there proper error handling?
  • Are there any race conditions?
  • Is sensitive data encrypted at rest?
  • Are network communications using TLS?
  • Are there proper access controls?

Bitcoin-Specific Review Points

  • Are transaction amounts validated against limits?
  • Is the fee calculation correct?
  • Are UTXO selections optimal?
  • Is signature generation using RFC6979 (deterministic)?
  • Are all transaction fields properly set?
  • Is the correct network (mainnet/testnet/regtest) being used?
  • Are dust outputs prevented?
  • Is RBF signaling correct?
  • Are timelocks and sequence numbers used correctly?
  • Is there proper handling of unconfirmed transactions?

Review Process

  1. Automated checks first: Run linters, formatters, and tests
  2. Security review: Focus on security implications
  3. Logic review: Verify business logic is correct
  4. Performance review: Check for optimization opportunities
  5. Documentation review: Ensure code is well-documented
  6. Test coverage review: Verify adequate testing

Production Readiness Checklist

Before deploying to production, ensure your application meets these requirements:

Production Readiness Checklist

⚠️ Never Skip These

Some items cannot be compromised:

  • Security audit: Have an expert review your code
  • Testnet testing: Always test on testnet first
  • Key management: Use proper HSM/hardware wallet solution
  • Backup procedures: Test your disaster recovery plan
  • Monitoring: You must know when things break

Deployment Considerations

Environment Configuration

# config.py - Never commit secrets!
import os

class Config:
    # Bitcoin Core connection
    BITCOIN_RPC_HOST = os.getenv('BITCOIN_RPC_HOST', 'localhost')
    BITCOIN_RPC_PORT = os.getenv('BITCOIN_RPC_PORT', '8332')
    BITCOIN_RPC_USER = os.getenv('BITCOIN_RPC_USER')
    BITCOIN_RPC_PASSWORD = os.getenv('BITCOIN_RPC_PASSWORD')

    # Network selection
    NETWORK = os.getenv('BITCOIN_NETWORK', 'mainnet')

    # Database
    DATABASE_URL = os.getenv('DATABASE_URL')

    # Security
    ENCRYPTION_KEY = os.getenv('ENCRYPTION_KEY')

    # Feature flags
    ENABLE_RBF = os.getenv('ENABLE_RBF', 'true').lower() == 'true'

    @classmethod
    def validate(cls):
        """Validate required configuration"""
        required = [
            'BITCOIN_RPC_USER',
            'BITCOIN_RPC_PASSWORD',
            'DATABASE_URL',
            'ENCRYPTION_KEY'
        ]
        missing = [key for key in required if not getattr(cls, key)]
        if missing:
            raise ValueError(f"Missing required config: {missing}")

Gradual Rollout Strategy

  1. Deploy to staging: Exact copy of production, safe testing environment
  2. Canary deployment: Route small percentage of traffic to new version
  3. Monitor metrics: Watch error rates, latency, transaction success
  4. Gradual rollout: Increase traffic percentage incrementally
  5. Full deployment: 100% traffic only after success metrics
  6. Rollback plan: One-click revert to previous version

Key Takeaways

  • Security is not optional—it's the foundation of Bitcoin applications
  • Never store or log private keys in plaintext
  • Validate all inputs—never trust user data
  • Test thoroughly: unit, integration, property-based, and fuzzing
  • Use defensive programming—assume operations will fail
  • Implement comprehensive error handling with specific exception types
  • All code should be reviewed by multiple people
  • Use hardware security modules or hardware wallets for key storage
  • Test on testnet extensively before mainnet deployment
  • Have incident response and disaster recovery plans ready
  • Monitor everything—you need to know when things break
  • Deploy gradually with ability to rollback quickly

Congratulations!

You've completed Stage 3 of the Builder Path. You now have the knowledge and tools to build production-ready Bitcoin applications—from setting up development environments to deploying secure, tested code.

But building applications is just the beginning. The next stage takes you deeper: contributing to Bitcoin Core itself, understanding consensus rules, and becoming part of the protocol development community.

You're now a Bitcoin Builder. You can create applications that handle real value, secure user funds, and contribute to Bitcoin's ecosystem. That's a significant achievement—wear it with pride and responsibility.