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
- Automated checks first: Run linters, formatters, and tests
- Security review: Focus on security implications
- Logic review: Verify business logic is correct
- Performance review: Check for optimization opportunities
- Documentation review: Ensure code is well-documented
- 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
- Deploy to staging: Exact copy of production, safe testing environment
- Canary deployment: Route small percentage of traffic to new version
- Monitor metrics: Watch error rates, latency, transaction success
- Gradual rollout: Increase traffic percentage incrementally
- Full deployment: 100% traffic only after success metrics
- 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.