Module 2: Building with Bitcoin

From Theory to Practice

You've mastered Bitcoin's protocol. You have a development environment. Now it's time to build something real.

In this module, you'll create a functional Bitcoin wallet from scratch. You'll generate HD wallets using industry standards (BIP32/39/44), manage UTXOs, construct and sign transactions, estimate fees, and broadcast to the network.

This is the most hands-on module yet. By the end, you'll have built software that creates real Bitcoin transactions. This is what separates Bitcoin enthusiasts from Bitcoin developers.

HD Wallet Generation: BIP32/39/44

Modern Bitcoin wallets use Hierarchical Deterministic (HD) wallets. From a single seed phrase, you can generate billions of addresses deterministically. Lose your phone? Restore everything from 12 words.

The BIP Standards

BIP39: Mnemonic Code

Converts entropy into human-readable seed phrases (12-24 words). "witch collapse practice feed shame open despair creek road again ice least" is easier to back up than raw hex.

Entropy (128 bits) → Checksum → Mnemonic Words (12)
Mnemonic + Passphrase → PBKDF2 → Seed (512 bits)

BIP32: Hierarchical Deterministic Wallets

Derives child keys from a master key using a derivation path. Each level represents a different purpose (purpose, coin type, account, change, address index).

Master Key → m/44'/0'/0'/0/0
             → m/44'/0'/0'/0/1
             → m/44'/0'/0'/0/2
             ... infinite addresses from one seed!

BIP44: Multi-Account Hierarchy

Defines a standard derivation path structure:

m / purpose' / coin_type' / account' / change / address_index

Example:
m/44'/0'/0'/0/0  - First receiving address
m/44'/0'/0'/1/0  - First change address
m/84'/0'/0'/0/0  - First native SegWit address (P2WPKH)

Implementing HD Wallet Generation

Let's build a HD wallet generator in Python:

from mnemonic import Mnemonic
from bip32 import BIP32
import hashlib

class BitcoinHDWallet:
    def __init__(self, mnemonic=None, passphrase=""):
        """Create HD wallet from mnemonic or generate new one"""
        self.mnemo = Mnemonic("english")

        if mnemonic is None:
            # Generate new mnemonic (128 bits = 12 words)
            mnemonic = self.mnemo.generate(strength=128)

        self.mnemonic = mnemonic
        self.passphrase = passphrase

        # Convert mnemonic to seed
        self.seed = self.mnemo.to_seed(mnemonic, passphrase)

        # Create BIP32 master key
        self.master_key = BIP32.from_seed(self.seed)

    def get_address(self, purpose=84, coin=0, account=0, change=0, index=0):
        """
        Derive address using BIP44/84 path
        purpose: 44 (P2PKH), 49 (P2SH-P2WPKH), 84 (P2WPKH)
        """
        # Build derivation path
        path = f"m/{purpose}'/{coin}'/{account}'/{change}/{index}"

        # Derive child key
        child_key = self.master_key.get_privkey_from_path(path)
        pubkey = self.master_key.get_pubkey_from_path(path)

        # Convert to address (this is simplified)
        address = self._pubkey_to_address(pubkey, purpose)

        return {
            'path': path,
            'address': address,
            'pubkey': pubkey.hex(),
            'privkey': child_key.hex()
        }

    def get_xpub(self, purpose=84, coin=0, account=0):
        """Get extended public key for account"""
        path = f"m/{purpose}'/{coin}'/{account}'"
        return self.master_key.get_xpub_from_path(path)

# Example usage
wallet = BitcoinHDWallet()
print(f"Mnemonic: {wallet.mnemonic}")
print(f"Master Key: {wallet.master_key.get_xpriv()}")

# Generate receiving addresses
for i in range(5):
    addr = wallet.get_address(purpose=84, index=i)
    print(f"Address {i}: {addr['address']}")
    print(f"  Path: {addr['path']}")

Security Critical

Never expose private keys or seed phrases in production code. This example is for education. Real wallets should:

  • Store seeds encrypted at rest
  • Use hardware security modules (HSMs) for production
  • Implement secure key derivation
  • Never log private keys or seeds
  • Use secure random number generation

UTXO Selection and Coin Control

When creating a transaction, you need to select which UTXOs to spend. This is called coin selection or coin control. The strategy you choose affects fees, privacy, and transaction size.

UTXO Selection Strategies

1. Largest First (Minimize Inputs)

  • Selects largest UTXOs first
  • Minimizes transaction size (fewer inputs)
  • Poor for privacy (reveals large holdings)
  • Good for reducing UTXO fragmentation

2. Smallest First (Privacy)

  • Selects smallest UTXOs first
  • Better privacy (doesn't reveal large UTXOs)
  • May require more inputs (higher fees)
  • Good for consolidation

3. Branch and Bound (Bitcoin Core Default)

  • Finds exact match when possible (no change output)
  • Optimizes for privacy and fees
  • Computationally expensive but worth it
  • Falls back to other strategies if no exact match

Implementing UTXO Selection

class UTXOSelector:
    def __init__(self, utxos):
        """Initialize with list of UTXOs"""
        self.utxos = sorted(utxos, key=lambda u: u['amount'], reverse=True)

    def select_for_amount(self, target_amount, fee_rate):
        """
        Select UTXOs to meet target amount + fees
        Returns: (selected_utxos, total_amount, estimated_fee)
        """
        selected = []
        total = 0

        for utxo in self.utxos:
            selected.append(utxo)
            total += utxo['amount']

            # Calculate estimated fee
            estimated_fee = self._estimate_fee(len(selected), 2, fee_rate)

            # Check if we have enough
            if total >= target_amount + estimated_fee:
                return selected, total, estimated_fee

        raise ValueError("Insufficient funds")

    def _estimate_fee(self, num_inputs, num_outputs, fee_rate):
        """
        Estimate transaction fee
        fee_rate in sat/vB
        """
        # P2WPKH input: ~68 vBytes
        # P2WPKH output: ~31 vBytes
        # Overhead: ~11 vBytes

        input_size = num_inputs * 68
        output_size = num_outputs * 31
        overhead = 11

        total_vbytes = input_size + output_size + overhead
        return total_vbytes * fee_rate

    def branch_and_bound(self, target_amount, fee_rate, max_iterations=1000):
        """
        Branch and Bound algorithm to find exact match
        Avoids change output when possible
        """
        # Calculate target with fees
        base_fee = self._estimate_fee(1, 1, fee_rate)
        target = target_amount + base_fee

        # Try to find exact match
        def search(index, current_utxos, current_sum):
            if current_sum == target:
                return current_utxos

            if current_sum > target or index >= len(self.utxos):
                return None

            # Try including this UTXO
            with_utxo = search(
                index + 1,
                current_utxos + [self.utxos[index]],
                current_sum + self.utxos[index]['amount']
            )
            if with_utxo:
                return with_utxo

            # Try excluding this UTXO
            return search(index + 1, current_utxos, current_sum)

        result = search(0, [], 0)
        if result:
            return result, target, base_fee

        # No exact match, fall back to largest-first
        return self.select_for_amount(target_amount, fee_rate)

# Example usage
utxos = [
    {'txid': 'abc...', 'vout': 0, 'amount': 100000},
    {'txid': 'def...', 'vout': 1, 'amount': 50000},
    {'txid': 'ghi...', 'vout': 0, 'amount': 25000},
    {'txid': 'jkl...', 'vout': 2, 'amount': 10000},
]

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

print(f"Selected {len(selected)} UTXOs")
print(f"Total input: {total} sats")
print(f"Estimated fee: {fee} sats")
print(f"Change: {total - 60000 - fee} sats")

Transaction Construction and Signing

Now we build the actual transaction. Bitcoin transactions are surprisingly simple at their core: inputs + outputs + signatures = valid transaction.

Transaction Structure Review

Transaction {
  version: 2
  inputs: [
    {
      previous_txid: "abc123..."
      previous_vout: 0
      scriptSig: ""  (empty for SegWit)
      witness: [...signatures, ...pubkeys...]
      sequence: 0xfffffffd
    }
  ]
  outputs: [
    {
      amount: 50000  (satoshis)
      scriptPubKey: "0014..." (P2WPKH address)
    },
    {
      amount: 25000  (change)
      scriptPubKey: "0014..."
    }
  ]
  locktime: 0
}

Building a Transaction

import hashlib
from bitcoin import SelectParams
from bitcoin.core import CTransaction, CTxIn, CTxOut, COutPoint, COIN
from bitcoin.core.script import CScript, OP_0, Hash160
from bitcoin.wallet import P2WPKHBitcoinAddress

class TransactionBuilder:
    def __init__(self, network='regtest'):
        """Initialize transaction builder"""
        SelectParams(network)
        self.tx_ins = []
        self.tx_outs = []

    def add_input(self, txid, vout, sequence=0xfffffffd):
        """Add input to transaction"""
        # Convert txid string to bytes (reversed for Bitcoin)
        txid_bytes = bytes.fromhex(txid)[::-1]

        # Create outpoint (reference to previous output)
        outpoint = COutPoint(txid_bytes, vout)

        # Create input
        tx_in = CTxIn(outpoint, nSequence=sequence)
        self.tx_ins.append(tx_in)

    def add_output(self, address, amount):
        """Add output to transaction"""
        # Convert address to scriptPubKey
        addr = P2WPKHBitcoinAddress(address)
        script = addr.to_scriptPubKey()

        # Create output
        tx_out = CTxOut(amount, script)
        self.tx_outs.append(tx_out)

    def create_transaction(self, locktime=0):
        """Create unsigned transaction"""
        tx = CTransaction(
            self.tx_ins,
            self.tx_outs,
            nLockTime=locktime,
            nVersion=2
        )
        return tx

    def sign_input(self, tx, input_index, private_key, utxo_amount, utxo_script):
        """
        Sign a SegWit input
        This is simplified - real implementation needs full BIP143 sighash
        """
        # Create signature hash for this input
        sighash = self._calculate_sighash_segwit(
            tx, input_index, utxo_amount, utxo_script
        )

        # Sign with private key
        signature = private_key.sign(sighash)

        # Add signature to witness
        tx.wit.vtxinwit[input_index].scriptWitness = CScriptWitness([
            signature,
            private_key.pub.serialize()
        ])

        return tx

# Example: Build a transaction
builder = TransactionBuilder('regtest')

# Add input (spending from previous transaction)
builder.add_input(
    txid="abc123def456...",
    vout=0
)

# Add output (payment)
builder.add_output(
    address="bcrt1q...",  # Recipient address
    amount=50000  # 0.0005 BTC
)

# Add output (change)
builder.add_output(
    address="bcrt1q...",  # Your change address
    amount=48000  # Remaining amount after fee
)

# Create transaction
tx = builder.create_transaction()

# Sign inputs (requires private keys)
# signed_tx = builder.sign_input(tx, 0, private_key, utxo_amount, utxo_script)

# Serialize for broadcasting
print(f"Raw transaction: {tx.serialize().hex()}")

⚠️ Transaction Signing is Complex

The signing process involves multiple steps and cryptographic operations. In production:

  • Use battle-tested libraries (python-bitcoinlib, bitcoinjs-lib, rust-bitcoin)
  • Implement BIP143 (SegWit signature hashing) correctly
  • Handle different script types (P2PKH, P2SH, P2WPKH, P2WSH)
  • Validate all inputs before signing
  • Use hardware wallets for key storage when possible

Fee Estimation and RBF

Bitcoin fees are a market—pay too little and your transaction gets stuck. Pay too much and you waste money. Getting fees right is crucial for good UX.

How Bitcoin Fees Work

  • Fees = Total Input Amount - Total Output Amount
  • Fee rate measured in satoshis per virtual byte (sat/vB)
  • Miners prioritize higher fee-rate transactions
  • Transaction weight depends on inputs, outputs, and script types

Fee Estimation Strategies

import requests

class FeeEstimator:
    def __init__(self, bitcoin_rpc):
        self.rpc = bitcoin_rpc

    def estimate_smart_fee(self, target_blocks=6):
        """
        Use Bitcoin Core's smart fee estimation
        target_blocks: confirmation target (1=next block, 6=~1 hour)
        """
        result = self.rpc.estimatesmartfee(target_blocks)

        if 'feerate' in result:
            # Convert BTC/kB to sat/vB
            btc_per_kb = result['feerate']
            sat_per_byte = (btc_per_kb * 100000000) / 1000
            return sat_per_byte

        # Fallback if estimation not available
        return 10  # 10 sat/vB default

    def get_mempool_fees(self):
        """
        Analyze mempool to estimate appropriate fees
        Returns fee rates for different confirmation targets
        """
        mempool_info = self.rpc.getmempoolinfo()

        # Get fee histogram from mempool
        # This is simplified - real implementation would analyze mempool
        return {
            'fastest': 50,   # Next block
            'fast': 25,      # 1-2 blocks
            'medium': 10,    # 3-6 blocks
            'slow': 5        # 6+ blocks
        }

    def calculate_tx_fee(self, num_inputs, num_outputs, fee_rate):
        """
        Calculate absolute fee for transaction
        """
        # P2WPKH transaction sizes (approximate)
        base_size = 10  # Version, locktime, etc.
        input_size = num_inputs * 68  # ~68 vBytes per P2WPKH input
        output_size = num_outputs * 31  # ~31 vBytes per P2WPKH output

        total_vbytes = base_size + input_size + output_size
        total_fee = total_vbytes * fee_rate

        return int(total_fee)

# Example usage
estimator = FeeEstimator(bitcoin_rpc)

# Get recommended fee rate
fee_rate = estimator.estimate_smart_fee(target_blocks=3)
print(f"Recommended fee rate: {fee_rate} sat/vB")

# Calculate fee for specific transaction
fee = estimator.calculate_tx_fee(
    num_inputs=2,
    num_outputs=2,
    fee_rate=fee_rate
)
print(f"Total fee: {fee} sats ({fee/100000000} BTC)")

Replace-By-Fee (RBF)

RBF allows you to "bump" a stuck transaction by creating a new version with higher fees:

# Enable RBF when creating transaction (set sequence < 0xfffffffe)
builder.add_input(txid, vout, sequence=0xfffffffd)  # RBF-enabled

# Later, if transaction is stuck, create replacement:
# 1. Use same inputs
# 2. Increase fee (decrease change output)
# 3. Broadcast replacement

# Bitcoin Core RBF
result = bitcoin_rpc.bumpfee(
    txid,
    options={'fee_rate': 25}  # New fee rate in sat/vB
)

Broadcasting Transactions

Once your transaction is signed, you need to broadcast it to the network. There are several methods:

1. Bitcoin Core RPC

# Using bitcoin-cli
bitcoin-cli -regtest sendrawtransaction "hex_encoded_tx"

# Using Python RPC
txid = bitcoin_rpc.sendrawtransaction(raw_tx_hex)
print(f"Transaction broadcast: {txid}")

2. Public APIs (Testnet/Mainnet)

import requests

def broadcast_transaction(raw_tx_hex, network='testnet'):
    """Broadcast using public API"""
    if network == 'testnet':
        url = "https://blockstream.info/testnet/api/tx"
    else:
        url = "https://blockstream.info/api/tx"

    response = requests.post(url, data=raw_tx_hex)

    if response.status_code == 200:
        return response.text  # Returns txid
    else:
        raise Exception(f"Broadcast failed: {response.text}")

# Example
txid = broadcast_transaction(signed_tx.serialize().hex(), network='testnet')
print(f"Transaction broadcast: {txid}")

3. Direct P2P Broadcasting

For maximum privacy and reliability, broadcast directly to multiple peers:

# Connect to multiple Bitcoin nodes
# Send "tx" message with your transaction
# This requires implementing Bitcoin P2P protocol
# Libraries like python-bitcoinlib can help

Transaction Monitoring

After broadcasting, monitor your transaction:

  • Check if it appears in mempool
  • Monitor for confirmations
  • Be prepared to RBF if needed
  • Watch for chain reorganizations
  • Wait for sufficient confirmations (typically 6)

Project: Build a Bitcoin Wallet

Put everything together and build a functional command-line wallet!

Requirements:

  1. Wallet Generation: Create HD wallet from mnemonic (BIP39/44)
  2. Address Management: Generate receiving and change addresses
  3. Balance Checking: Query UTXOs and calculate balance
  4. Transaction Creation: Build transactions with proper UTXO selection
  5. Fee Estimation: Implement smart fee calculation
  6. Signing: Sign transactions with derived keys
  7. Broadcasting: Send transactions to network

Bonus Features:

  • Support multiple accounts
  • Implement RBF for stuck transactions
  • Add transaction history viewing
  • Create QR codes for addresses
  • Implement PSBT (Partially Signed Bitcoin Transactions)
View Starter Code
#!/usr/bin/env python3
"""
Simple Bitcoin HD Wallet
Usage: python wallet.py [command] [args]
"""

import sys
from bitcoin import SelectParams
from mnemonic import Mnemonic

class SimpleWallet:
    def __init__(self):
        SelectParams('regtest')
        self.wallet = None

    def create(self, mnemonic=None):
        """Create new wallet or restore from mnemonic"""
        if mnemonic is None:
            mnemo = Mnemonic("english")
            mnemonic = mnemo.generate(strength=128)
            print(f"🔑 New wallet created!")
            print(f"Mnemonic (SAVE THIS!): {mnemonic}")
        else:
            print(f"🔄 Wallet restored from mnemonic")

        # Initialize HD wallet
        # TODO: Implement HD wallet creation

    def get_address(self, index=0):
        """Get receiving address"""
        # TODO: Derive address from HD wallet
        pass

    def get_balance(self):
        """Get wallet balance"""
        # TODO: Query UTXOs and sum amounts
        pass

    def send(self, address, amount):
        """Send bitcoin to address"""
        # TODO:
        # 1. Select UTXOs
        # 2. Build transaction
        # 3. Sign transaction
        # 4. Broadcast transaction
        pass

if __name__ == "__main__":
    wallet = SimpleWallet()

    if len(sys.argv) < 2:
        print("Usage: wallet.py [create|balance|address|send]")
        sys.exit(1)

    command = sys.argv[1]

    if command == "create":
        wallet.create()
    elif command == "balance":
        print(f"Balance: {wallet.get_balance()} BTC")
    elif command == "address":
        print(f"Address: {wallet.get_address()}")
    elif command == "send" and len(sys.argv) == 4:
        address, amount = sys.argv[2], float(sys.argv[3])
        wallet.send(address, amount)
    else:
        print("Invalid command")

Key Takeaways

  • HD wallets (BIP32/39/44) allow infinite addresses from one seed
  • UTXO selection strategies balance fees, privacy, and usability
  • Transaction construction involves inputs, outputs, and witness data
  • Fee estimation should consider network conditions and confirmation urgency
  • RBF allows fee bumping for stuck transactions
  • Broadcasting can be done via RPC, APIs, or P2P network
  • Always use battle-tested libraries for cryptographic operations
  • Seed phrases must be stored securely and never exposed
  • Test thoroughly on regtest before using testnet or mainnet