Understanding the complete process from UTXO selection to transaction broadcast
Table of Contents
- Introduction
- Bitcoin Transaction Structure
- UTXO Selection Strategy
- Transaction Building Process
- The Signing Process
- Address Types and Scripts
- Security Considerations
- Common Pitfalls
- Best Practices
- 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)
}
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)
}
Output Structure
struct TxOut {
value: Amount, // Amount in satoshis
script_pubkey: ScriptBuf, // Locking script
}
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)
}
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(),
};
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(),
});
}
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,
});
}
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)?;
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)?;
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
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);
}
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)?;
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);
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();
Step 8: Attach to Transaction
// Attach the unlocking script to the input
tx.input[i].script_sig = script_sig;
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
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
orn
(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
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)?;
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);
}
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)?;
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)?;
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;
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);
}
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)?;
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));
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
}
4. Network Validation
// Always validate network compatibility
let address = Address::from_str(address_str)?
.require_network(network)?;
5. UTXO Management
// Implement proper UTXO tracking
- Update balances regularly
- Validate UTXO ownership
- Handle unconfirmed transactions
- Implement proper UTXO selection
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
- Security First: Private key management is critical
- Understand the Flow: From private key to signed transaction
- Network Validation: Always verify correct network
- Fee Optimization: Proper fee calculation is essential
- Error Handling: Graceful error handling prevents failures
- 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
- Bitcoin Developer Documentation
- Rust Bitcoin Library
- BIPs (Bitcoin Improvement Proposals)
- Bitcoin Core Source Code
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)