DEV Community

이관호(Gwanho LEE)
이관호(Gwanho LEE)

Posted on

Bitcoin Transaction Signing: A Developer's Deep Dive

Understanding the complete process from UTXO selection to transaction broadcast

Table of Contents

  1. Introduction
  2. Bitcoin Transaction Structure
  3. UTXO Selection Strategy
  4. Transaction Building Process
  5. The Signing Process
  6. Address Types and Scripts
  7. Security Considerations
  8. Common Pitfalls
  9. Best Practices
  10. Conclusion

Introduction

Bitcoin transaction signing is the core mechanism that enables secure, decentralized value transfer. As a blockchain developer, understanding this process is crucial for building reliable applications. This guide walks through the complete transaction signing process, from UTXO selection to transaction broadcast.

Bitcoin Transaction Structure

Basic Components

struct BitcoinTransaction {
    version: Version,        // Protocol version (usually 2)
    lock_time: LockTime,     // Time/block lock (0 for immediate)
    input: Vec<TxIn>,        // Inputs (UTXOs being spent)
    output: Vec<TxOut>,      // Outputs (where money goes)
}
Enter fullscreen mode Exit fullscreen mode

Input Structure

struct TxIn {
    previous_output: OutPoint,  // Which UTXO (txid + vout)
    script_sig: ScriptBuf,      // Unlocking script (signature + pubkey)
    sequence: Sequence,         // Relative locktime
    witness: Witness,           // SegWit data (empty for legacy)
}
Enter fullscreen mode Exit fullscreen mode

Output Structure

struct TxOut {
    value: Amount,              // Amount in satoshis
    script_pubkey: ScriptBuf,   // Locking script
}
Enter fullscreen mode Exit fullscreen mode

UTXO Selection Strategy

Why Multiple UTXOs?

Bitcoin transactions often require spending multiple UTXOs because:

  • No single UTXO has sufficient value
  • Need to optimize for fees
  • Want to minimize change output size

Selection Algorithms

1. Largest First (Most Common)

fn select_optimal_utxos(utxos: &[UTXO], target: u64, fee_rate: u64) -> Vec<UTXO> {
    let mut sorted = utxos.to_vec();
    sorted.sort_by(|a, b| b.value.cmp(&a.value)); // Largest first

    let mut selected = Vec::new();
    let mut total = 0u64;

    for utxo in sorted {
        selected.push(utxo.clone());
        total += utxo.value;

        let estimated_fee = selected.len() as u64 * 200 * fee_rate;
        if total >= target + estimated_fee {
            return selected;
        }
    }

    Err(InsufficientFunds)
}
Enter fullscreen mode Exit fullscreen mode

2. Branch and Bound (Bitcoin Core)

More sophisticated algorithm that minimizes change output size.

3. Coin Selection Considerations

  • Privacy: Smaller UTXOs first for better privacy
  • Efficiency: Fewer inputs = lower fees
  • Change: Minimize change output size

Transaction Building Process

Phase 1: Transaction Initialization

let mut tx = BitcoinTransaction {
    version: Version::TWO,
    lock_time: LockTime::ZERO,
    input: Vec::new(),
    output: Vec::new(),
};
Enter fullscreen mode Exit fullscreen mode

Phase 2: Add Inputs (Empty Scripts)

for (_, utxo) in &selected_utxos {
    let txid = bitcoin::Txid::from_str(&utxo.txid)?;
    let outpoint = OutPoint::new(txid, utxo.vout);

    tx.input.push(TxIn {
        previous_output: outpoint,
        script_sig: ScriptBuf::new(), // Empty initially
        sequence: Sequence::ZERO,
        witness: Witness::new(),
    });
}
Enter fullscreen mode Exit fullscreen mode

Phase 3: Add Outputs

// Destination output
let dest_script = create_script_pubkey(dest_address, Network::Testnet)?;
tx.output.push(TxOut {
    value: Amount::from_sat(amount),
    script_pubkey: dest_script,
});

// Change output (if needed)
if change > 0 {
    let change_script = create_script_pubkey(&change_address.address, Network::Testnet)?;
    tx.output.push(TxOut {
        value: Amount::from_sat(change),
        script_pubkey: change_script,
    });
}
Enter fullscreen mode Exit fullscreen mode

The Signing Process

Understanding the Flow

The signing process is the heart of Bitcoin's security model. Here's the complete flow:

Step 1: Extract Private Key

// Get private key from Wallet Import Format (WIF)
let private_key = PrivateKey::from_wif(&addr.private_key)?;
Enter fullscreen mode Exit fullscreen mode

Why WIF Format?

  • Standard format used by all Bitcoin wallets
  • Includes checksums for error detection
  • Different prefixes for mainnet/testnet
  • Indicates public key compression

Step 2: Create Script PubKey for Input

// Create script_pubkey for the address we're spending from
let script_pubkey = create_script_pubkey(&addr.address, Network::Testnet)?;
Enter fullscreen mode Exit fullscreen mode

Step 3: Derive Public Key

// Get public key from private key using elliptic curve math
let pubkey = private_key.public_key(&secp);
// secp256k1: private_key * G = public_key
Enter fullscreen mode Exit fullscreen mode

Step 4: Verify Address Ownership

// Verify the private key corresponds to the address
let pubkey_hash_from_address = address.pubkey_hash()?;
let pubkey_hash_from_private_key = hash160::Hash::hash(&pubkey.inner.serialize());

if pubkey_hash_from_address != pubkey_hash_from_private_key {
    return Err(WalletError::TransactionFailed);
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Create Sighash

// Create cryptographic hash of transaction data
let cache = SighashCache::new(&tx);
let sighash = cache.legacy_signature_hash(i, &script_pubkey, EcdsaSighashType::All as u32)?;
Enter fullscreen mode Exit fullscreen mode

What Gets Hashed:

  • Transaction version
  • All input transaction IDs and output indices
  • All input sequences
  • All output values and scripts
  • Transaction locktime
  • The specific input's script (script_pubkey)

Step 6: Sign the Hash

// Convert sighash to message and sign with private key
let msg = Message::from_digest_slice(sighash.as_ref())?;
let sig = secp.sign_ecdsa(&msg, &private_key.inner);
Enter fullscreen mode Exit fullscreen mode

Step 7: Create Script Signature

// Prepare signature and public key bytes
let mut sig_bytes = sig.serialize_der().to_vec();
sig_bytes.push(EcdsaSighashType::All as u8);  // Add hash type
let pubkey_bytes = pubkey.inner.serialize();  // Compressed public key

// Create the unlocking script
let script_sig = bitcoin::script::Builder::new()
    .push_slice(sig_bytes)      // Push signature
    .push_slice(pubkey_bytes)   // Push public key
    .into_script();
Enter fullscreen mode Exit fullscreen mode

Step 8: Attach to Transaction

// Attach the unlocking script to the input
tx.input[i].script_sig = script_sig;
Enter fullscreen mode Exit fullscreen mode

Visual Transaction Flow

🔒 OUTPUTS (Locking Scripts):
Address → Script PubKey (locks money to address)

🔓 INPUTS (Unlocking Scripts):
Private Key (WIF) → Public Key (elliptic curve)
    ↓
Verify: Address pubkey_hash == Private Key pubkey_hash
    ↓
Create sighash (transaction hash)
    ↓
Sign sighash with private key
    ↓
Create script_sig = [signature] [public_key]
    ↓
Attach to transaction input
Enter fullscreen mode Exit fullscreen mode

Address Types and Scripts

P2PKH (Pay to Public Key Hash)

Most common legacy address type.

Address Format

  • Mainnet: Starts with 1 (e.g., 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa)
  • Testnet: Starts with m or n (e.g., mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn)

Script Structure

// ScriptPubKey (locking script)
OP_DUP           // Duplicate the public key
OP_HASH160       // Hash the public key
[pubkey_hash]    // The actual hash (20 bytes)
OP_EQUALVERIFY   // Verify the hash matches
OP_CHECKSIG      // Verify the signature

// ScriptSig (unlocking script)
[signature]      // ECDSA signature
[public_key]     // Full public key
Enter fullscreen mode Exit fullscreen mode

SegWit Addresses

Newer address types with lower fees.

P2WPKH (Pay to Witness Public Key Hash)

  • Mainnet: Starts with bc1 (e.g., bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4)
  • Testnet: Starts with tb1

SegWit Benefits

  • Lower fees: Witness data not counted in fee calculation
  • Better privacy: Separates signature data from transaction data
  • Malleability fix: Transaction ID cannot be changed after signing

Security Considerations

Private Key Security

// CRITICAL: Whoever has your private key can spend all your UTXOs
let private_key = PrivateKey::from_wif(&addr.private_key)?;
Enter fullscreen mode Exit fullscreen mode

Best Practices:

  • Store private keys securely (hardware wallets, encrypted storage)
  • Never share private keys
  • Use deterministic wallets (BIP32/BIP39)
  • Implement proper key derivation

Address Verification

// Always verify address ownership before signing
let pubkey_hash_from_address = address.pubkey_hash()?;
let pubkey_hash_from_private_key = hash160::Hash::hash(&pubkey.inner.serialize());

if pubkey_hash_from_address != pubkey_hash_from_private_key {
    return Err(WalletError::TransactionFailed);
}
Enter fullscreen mode Exit fullscreen mode

Transaction Validation

  • Verify all inputs belong to you
  • Check sufficient funds
  • Validate fee calculation
  • Ensure proper network (mainnet vs testnet)

Common Pitfalls

1. Incorrect Sighash Calculation

// ❌ Wrong: Using value parameter for legacy P2PKH
let sighash = cache.legacy_signature_hash(i, &script_pubkey, EcdsaSighashType::All as u32)?;

// ✅ Correct: No value parameter for legacy
let sighash = cache.legacy_signature_hash(i, &script_pubkey, EcdsaSighashType::All as u32)?;
Enter fullscreen mode Exit fullscreen mode

2. Wrong Network

// ❌ Wrong: Using mainnet address on testnet
let address = Address::from_str("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")?;

// ✅ Correct: Validate network
let address = Address::from_str("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")?
    .require_network(Network::Bitcoin)?;
Enter fullscreen mode Exit fullscreen mode

3. Insufficient Fee Calculation

// ❌ Wrong: Ignoring output size
let fee = inputs.len() * 200 * fee_rate;

// ✅ Correct: Include all transaction components
let fee = (10 + inputs.len() * 148 + outputs.len() * 34) * fee_rate;
Enter fullscreen mode Exit fullscreen mode

4. UTXO Double-Spending

// ❌ Wrong: Not checking if UTXO is already spent
// Always validate UTXO status before using

// ✅ Correct: Check UTXO status
if !utxo.status.confirmed {
    return Err(WalletError::UTXONotConfirmed);
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Error Handling

// Always handle errors gracefully
let sighash = cache.legacy_signature_hash(i, &script_pubkey, EcdsaSighashType::All as u32)
    .map_err(|_| WalletError::TransactionFailed)?;
Enter fullscreen mode Exit fullscreen mode

2. Logging and Debugging

// Add comprehensive logging for debugging
println!("Signing input {} with address {}", i, addr.address);
println!("Transaction hash: {}", hex::encode(sighash));
println!("Signature: {}", hex::encode(&sig_bytes));
Enter fullscreen mode Exit fullscreen mode

3. Fee Optimization

// Use appropriate fee calculation
fn calculate_fee(inputs: usize, outputs: usize, fee_rate: u64) -> u64 {
    let size = 10 + (inputs * 148) + (outputs * 34);
    size as u64 * fee_rate
}
Enter fullscreen mode Exit fullscreen mode

4. Network Validation

// Always validate network compatibility
let address = Address::from_str(address_str)?
    .require_network(network)?;
Enter fullscreen mode Exit fullscreen mode

5. UTXO Management

// Implement proper UTXO tracking
- Update balances regularly
- Validate UTXO ownership
- Handle unconfirmed transactions
- Implement proper UTXO selection
Enter fullscreen mode Exit fullscreen mode

Conclusion

Bitcoin transaction signing is a complex but essential process for blockchain developers. Understanding the complete flow from UTXO selection to transaction broadcast is crucial for building reliable applications.

Key Takeaways

  1. Security First: Private key management is critical
  2. Understand the Flow: From private key to signed transaction
  3. Network Validation: Always verify correct network
  4. Fee Optimization: Proper fee calculation is essential
  5. Error Handling: Graceful error handling prevents failures
  6. Testing: Thorough testing on testnet before mainnet

Next Steps

  • Implement SegWit support
  • Add multi-signature support
  • Explore advanced scripting (P2SH, P2WSH)
  • Study Lightning Network for microtransactions
  • Learn about coin selection algorithms

Resources


This guide covers the essential concepts every blockchain developer should understand about Bitcoin transaction signing. Master these fundamentals, and you'll have a solid foundation for building Bitcoin applications.

Top comments (0)