Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Transaction Processing

One of Bitcoin’s core innovations is the UTXO (Unspent Transaction Output) model. Unlike a bank account balance, there is no concept of an “account balance” in the Bitcoin system — all funds exist in a distributed form across the blockchain as unspent transaction outputs. This chapter provides an in-depth look at SimpleBTC’s transaction structure, UTXO model, and transaction creation and validation mechanisms.


UTXO Model Fundamentals

What is a UTXO

UTXO stands for “Unspent Transaction Output.” Every Bitcoin transaction consumes some existing UTXOs (as inputs) and simultaneously creates new UTXOs (as outputs).

┌──────────────────────────────────────────────────────────────────┐
│ Traditional Account Model (e.g., banking)                         │
│  Alice balance: 100        Bob balance: 0                        │
│  Transfer 30 → Alice: 70, Bob: 30                                │
├──────────────────────────────────────────────────────────────────┤
│ UTXO Model (Bitcoin)                                              │
│  On-chain: UTXO_A (owned by Alice, value 100)                    │
│  Transfer 30:                                                     │
│    Consume: UTXO_A (100)  ← must be consumed in full             │
│    Create:  UTXO_B (30, owned by Bob)   ← transfer amount        │
│             UTXO_C (60, owned by Alice) ← change (100 - 30 - 10 fee)│
└──────────────────────────────────────────────────────────────────┘

Key properties of the UTXO model:

  • Each UTXO can only be spent once (removed from the UTXO set after spending)
  • A UTXO must be consumed in full when spent; any excess is returned to the sender as a “change” output
  • “Balance” = the sum of all UTXO values owned by an address (calculated by UTXOSet)
  • Unspent UTXOs form the “UTXO set” (a Bitcoin full node must maintain approximately 5-10 GB of UTXO set data)

Transaction Data Structures

TxInput (Transaction Input)

A transaction input references an existing UTXO and provides proof of authorization to spend it (a digital signature):

#![allow(unused)]
fn main() {
// src/transaction.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxInput {
    pub txid: String,       // ID of the referenced transaction (32-byte hash, 64 hex characters)
    pub vout: usize,        // Index of the output within that transaction (0-based)
    pub signature: String,  // ECDSA signature (DER-encoded, hex string)
    pub pub_key: String,    // Sender's compressed public key (33 bytes, 66 hex characters)
}
}

The combination of txid + vout uniquely identifies a UTXO on the blockchain. The signature is generated by the sender’s private key, proving ownership of that UTXO.

TxOutput (Transaction Output)

A transaction output defines the amount the recipient can receive; it is the carrier of a new UTXO:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxOutput {
    pub value: u64,           // Amount (unit: satoshi; 1 BTC = 10⁸ satoshi)
    pub pub_key_hash: String, // Recipient address (P2PKH) = locking script
}

impl TxOutput {
    pub fn new(value: u64, address: String) -> Self {
        TxOutput { value, pub_key_hash: address }
    }

    /// Check whether this output can be unlocked by a given address
    /// (i.e., whether that address is the recipient of this output)
    pub fn can_be_unlocked_with(&self, address: &str) -> bool {
        self.pub_key_hash == address
    }
}
}

pub_key_hash is the locking script (scriptPubKey) in real Bitcoin; here it is simplified to the recipient’s address.

Transaction

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
    pub id: String,              // Transaction ID = SHA-256(transaction content)
    pub inputs: Vec<TxInput>,    // Input list (which UTXOs to consume)
    pub outputs: Vec<TxOutput>,  // Output list (which new UTXOs to create)
    pub timestamp: u64,          // Unix timestamp (seconds)
    pub fee: u64,                // Fee = total inputs - total outputs
}
}

Invariant: total inputs = total outputs + fee


Creating a Regular Transaction

Regular transactions are created via Blockchain::create_transaction(), which automatically handles UTXO selection, change calculation, and signing:

#![allow(unused)]
fn main() {
pub fn create_transaction(
    &self,
    from_wallet: &Wallet,   // Sender's wallet (private key required for signing)
    to_address: String,     // Recipient's address
    amount: u64,            // Transfer amount (satoshi)
    fee: u64,               // Transaction fee (satoshi)
) -> Result<Transaction, String>
}

Internal flow:

#![allow(unused)]
fn main() {
// Excerpt from src/blockchain.rs (simplified)

// 1. Calculate total needed
let total_needed = amount + fee;

// 2. Find sufficient UTXOs in the UTXO set (excluding those already spent by pending transactions)
let spendable = self.utxo_set.find_spendable_outputs_excluding(
    &from_wallet.address,
    total_needed,
    &self.pending_spent,
);
let (accumulated, utxos) = spendable.ok_or_else(|| "Insufficient balance (including fee)".to_string())?;

// 3. Create an input for each selected UTXO and sign it
let mut inputs = Vec::new();
for (txid, vout) in utxos {
    let signature = from_wallet.sign(&format!("{}{}", txid, vout));
    let input = TxInput::new(txid, vout, signature, from_wallet.public_key.clone());
    inputs.push(input);
}

// 4. Create outputs (transfer + change)
let mut outputs = Vec::new();
outputs.push(TxOutput::new(amount, to_address));
if accumulated > total_needed {
    // Return change to the sender (minus the fee)
    outputs.push(TxOutput::new(accumulated - total_needed, from_wallet.address.clone()));
}

// 5. Construct the transaction (ID is computed automatically)
Ok(Transaction::new(inputs, outputs, timestamp, fee))
}

Complete usage example:

#![allow(unused)]
fn main() {
use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};

let mut blockchain = Blockchain::new();
let genesis = Blockchain::genesis_wallet(); // Pre-funded with 10M satoshi
let alice = Wallet::new();

// genesis transfers 5000 satoshi to alice, with a fee of 100
let tx = blockchain.create_transaction(
    &genesis,
    alice.address.clone(),
    5000,   // Transfer amount
    100,    // Fee
)?;

println!("Transaction ID: {}", tx.id);
println!("Number of inputs: {}", tx.inputs.len());
println!("Number of outputs: {}", tx.outputs.len()); // Usually 2 (transfer + change)
println!("Fee: {} satoshi", tx.fee);
println!("Fee rate: {:.2} sat/byte", tx.fee_rate());
}

Coinbase Transaction

A Coinbase transaction is the first transaction in every block, dedicated to paying the block reward to the miner. Key differences from a regular transaction:

FeatureRegular TransactionCoinbase Transaction
InputsReference existing UTXOsEmpty txid (created from nothing)
OutputsTransfer + changeBlock reward + all fees
SignatureECDSA signatureNo signature required (pub_key = "coinbase")
ValidationAll input signatures verifiedSignature verification skipped (is_coinbase() = true)
#![allow(unused)]
fn main() {
pub fn new_coinbase(to: String, reward: u64, timestamp: u64, total_fees: u64) -> Self {
    // Use an atomic counter to ensure each coinbase transaction ID is unique
    // (similar to BIP34 block height encoding)
    static COINBASE_COUNTER: AtomicU64 = AtomicU64::new(0);
    let nonce = COINBASE_COUNTER.fetch_add(1, Ordering::Relaxed);

    // Output = block reward + all transaction fees
    let tx_out = TxOutput::new(reward + total_fees, to);
    let tx_in = TxInput {
        txid: String::new(),              // Empty txid identifies coinbase
        vout: 0,
        signature: format!("coinbase:{}", nonce),  // Uniqueness field
        pub_key: String::from("coinbase"),
    };
    // ...
}
}

How to identify a Coinbase transaction:

#![allow(unused)]
fn main() {
pub fn is_coinbase(&self) -> bool {
    // Only one input, and that input's txid is empty
    self.inputs.len() == 1 && self.inputs[0].txid.is_empty()
}
}

Transaction Validation

Transaction::verify() validates the ECDSA signatures of all inputs:

#![allow(unused)]
fn main() {
pub fn verify(&self) -> bool {
    // Coinbase transactions do not need signature verification
    if self.is_coinbase() {
        return true;
    }

    // Must have at least one input and one output
    if self.inputs.is_empty() || self.outputs.is_empty() {
        return false;
    }

    // Verify the ECDSA signature of each input
    for input in &self.inputs {
        // The signed data is the location identifier of the UTXO being spent
        let signed_data = format!("{}{}", input.txid, input.vout);
        if !Wallet::verify_signature(&input.pub_key, &signed_data, &input.signature) {
            return false;
        }
    }

    true
}
}

Validation logic explanation:

  • The signed data is "{txid}{vout}", binding the signature to a specific UTXO and preventing signature replay attacks
  • The public key carried in the input (pub_key) is used to verify the signature — full nodes do not need additional lookups
  • Wallet::verify_signature() internally calls secp256k1 to perform real elliptic curve mathematical verification

Complete validation flow when adding a transaction to the blockchain (Blockchain::add_transaction()):

#![allow(unused)]
fn main() {
// 1. ECDSA signature verification
if !transaction.verify() {
    return Err("Transaction verification failed".to_string());
}

// 2. Verify UTXO exists and balance is sufficient
let mut input_sum = 0u64;
for input in &transaction.inputs {
    if let Some(outputs) = self.find_transaction_outputs(&input.txid) {
        if let Some((_, output)) = outputs.iter().find(|(idx, _)| *idx == input.vout) {
            input_sum += output.value;
        } else {
            return Err("UTXO does not exist".to_string());
        }
    } else {
        return Err("Referenced transaction does not exist".to_string());
    }
}

let output_sum: u64 = transaction.outputs.iter().map(|o| o.value).sum();
if input_sum < output_sum {
    return Err("Insufficient balance, transaction is invalid".to_string());
}

// 3. Record pending UTXOs (prevent the same UTXO from being double-spent
//    by two pending transactions)
for input in &transaction.inputs {
    self.pending_spent.insert(format!("{}:{}", input.txid, input.vout));
}
}

Fees and Fee Rate

Fee Calculation

fee = total inputs - total outputs

Example: spend a UTXO (100 satoshi), transfer 85, change 5, fee = 100 - 85 - 5 = 10 satoshi

Fee Rate

#![allow(unused)]
fn main() {
pub fn fee_rate(&self) -> f64 {
    let size = self.size();  // Serialized transaction size in bytes
    if size == 0 { return 0.0; }
    self.fee as f64 / size as f64  // Unit: satoshi/byte
}
}

Typical fee rate reference (real Bitcoin — varies with network congestion):

PriorityFee RateConfirmation Time
Low1–5 sat/byteSeveral hours or longer
Medium5–20 sat/byte30–60 minutes
High20–50 sat/byte10–20 minutes
Urgent50+ sat/byteNext block (~10 minutes)

SimpleBTC’s mempool sorts transactions by fee rate, and higher fee-rate transactions are packaged first:

#![allow(unused)]
fn main() {
// Retrieve top transactions sorted by fee rate (Mempool internal logic)
let pending_txs = self.mempool.get_top_transactions(usize::MAX);
}

Transaction Lifecycle

User initiates a transfer request
       │
       ▼
blockchain.create_transaction(&wallet, to, amount, fee)
  → Select UTXOs, generate signatures, construct Transaction
       │
       ▼
blockchain.add_transaction(tx)
  → Verify signatures (ECDSA)
  → Verify UTXO exists + balance sufficient
  → Add to Mempool (sorted by fee rate)
  → Mark spent UTXOs (pending_spent)
       │
       ▼  Waiting to be packaged by a miner
       │
       ▼
blockchain.mine_pending_transactions(miner_address)
  → Retrieve high-priority transactions from Mempool
  → Create Coinbase transaction (reward + fees)
  → Parallel PoW mining
  → Update UTXO set (atomic operation)
  → Block appended to chain, pending_spent cleared
       │
       ▼
Transaction receives 1 confirmation
(each additional subsequent block = +1 confirmation)

Transaction Hash Calculation

#![allow(unused)]
fn main() {
pub fn calculate_hash(&self) -> String {
    // Serialize the transaction to JSON and compute SHA-256
    let tx_data = serde_json::to_string(&self).unwrap_or_default();
    let mut hasher = Sha256::new();
    hasher.update(tx_data.as_bytes());
    format!("{:x}", hasher.finalize())
}
}

Real Bitcoin uses double SHA-256 (SHA256d) with a compact binary serialization format. Here, for educational simplicity, JSON serialization and a single SHA-256 are used instead.


Output Sum Query

#![allow(unused)]
fn main() {
// Get the sum of all output amounts
let output_sum = tx.output_sum();

// Transaction size (in bytes, affects fee calculation)
let size = tx.size();

// Whether this is a Coinbase transaction
let is_cb = tx.is_coinbase();
}

Complete Transaction Example

use bitcoin_simulation::{blockchain::Blockchain, wallet::Wallet};

fn main() -> Result<(), String> {
    let mut blockchain = Blockchain::new();

    let genesis = Blockchain::genesis_wallet();
    let alice = Wallet::new();
    let bob = Wallet::new();

    // First transaction: genesis → alice, transfer 10000 satoshi
    let tx1 = blockchain.create_transaction(&genesis, alice.address.clone(), 10000, 50)?;
    println!("tx1 id: {}", tx1.id);
    println!("tx1 output count: {}", tx1.outputs.len()); // 2 (transfer + change)
    println!("tx1 fee rate: {:.2} sat/byte", tx1.fee_rate());
    blockchain.add_transaction(tx1)?;

    // Mine to confirm (alice receives the mining reward)
    blockchain.mine_pending_transactions(alice.address.clone())?;

    println!("Alice's balance: {} satoshi", blockchain.get_balance(&alice.address));

    // Second transaction: alice → bob, transfer 3000 satoshi
    let tx2 = blockchain.create_transaction(&alice, bob.address.clone(), 3000, 100)?;
    blockchain.add_transaction(tx2)?;

    blockchain.mine_pending_transactions(bob.address.clone())?;

    println!("Alice's balance: {} satoshi", blockchain.get_balance(&alice.address));
    println!("Bob's balance:   {} satoshi", blockchain.get_balance(&bob.address));

    // Verify chain integrity
    assert!(blockchain.is_valid());

    Ok(())
}