UTXO API
The UTXO module manages all unspent transaction outputs and is the core of Bitcoin’s accounting model.
Data Structures
UTXOSet
#![allow(unused)]
fn main() {
pub struct UTXOSet {
// key: txid (transaction ID)
// value: Vec<(vout_index, TxOutput)>
utxos: HashMap<String, Vec<(usize, TxOutput)>>,
}
}
Internal structure:
HashMap {
"tx1": [(0, Output{value: 100, addr: "alice"}),
(1, Output{value: 50, addr: "bob"})],
"tx2": [(0, Output{value: 200, addr: "charlie"})],
}
Core Concepts
UTXO Model vs. Account Model
Account Model (Ethereum)
Account balances:
Alice: 100 BTC
Bob: 50 BTC
Transfer: Alice → Bob (30 BTC)
Alice: 70 BTC (-30)
Bob: 80 BTC (+30)
UTXO Model (Bitcoin)
UTXO set:
tx1:0 → 100 BTC (Alice)
tx1:1 → 50 BTC (Bob)
Transfer: Alice → Bob (30 BTC)
Spend: tx1:0 (100 BTC)
Create:
tx2:0 → 30 BTC (Bob)
tx2:1 → 70 BTC (Alice, change)
New UTXO set:
tx1:1 → 50 BTC (Bob)
tx2:0 → 30 BTC (Bob)
tx2:1 → 70 BTC (Alice)
Advantages of the UTXO Model
-
Better privacy
- Can use a new address each time
- Harder to trace fund flows
-
Parallel processing
- Different UTXOs can be validated concurrently
- No account locking issues
-
Simplified validation
- Only need to check UTXO existence
- No need for account history
Methods
Initialization
new
#![allow(unused)]
fn main() {
pub fn new() -> Self
}
Creates an empty UTXO set.
Example:
#![allow(unused)]
fn main() {
use bitcoin_simulation::utxo::UTXOSet;
let mut utxo_set = UTXOSet::new();
}
UTXO Management
add_transaction
#![allow(unused)]
fn main() {
pub fn add_transaction(&mut self, tx: &Transaction)
}
Adds all outputs of a transaction to the UTXO set.
Process:
- Iterate over all outputs of the transaction
- Mark each output as unspent
- Add to the UTXO set
Parameters:
tx- The transaction to add
Note: Only adds outputs, does not process inputs
Example:
#![allow(unused)]
fn main() {
let tx = Transaction::new(...);
utxo_set.add_transaction(&tx);
// Now all of tx's outputs can be spent
}
remove_utxo
#![allow(unused)]
fn main() {
pub fn remove_utxo(&mut self, txid: &str, vout: usize)
}
Removes a spent UTXO.
Double-spend protection:
- A UTXO can only be spent once
- Removed from the set immediately after spending
- A second reference will fail
Parameters:
txid- Transaction IDvout- Output index
Example:
#![allow(unused)]
fn main() {
// Spend a UTXO
utxo_set.remove_utxo("tx1", 0);
// Attempt to spend again (fails)
// UTXO does not exist
}
process_transaction
#![allow(unused)]
fn main() {
pub fn process_transaction(&mut self, tx: &Transaction) -> bool
}
Fully processes a transaction (removes inputs, adds outputs).
ACID properties:
Atomicity:
#![allow(unused)]
fn main() {
// Either fully succeeds or fully fails
if !tx.verify() {
return false; // No modifications made
}
// All processed
}
Consistency:
#![allow(unused)]
fn main() {
// Before and after processing: input_sum = output_sum + fee
assert_eq!(input_sum, output_sum + fee);
}
Steps:
- Validate the transaction
- Remove UTXOs referenced by inputs
- Add newly created outputs
Parameters:
tx- The transaction to process
Return value:
true- Processing successfulfalse- Transaction is invalid
Example:
#![allow(unused)]
fn main() {
let tx = blockchain.create_transaction(&alice, bob.address, 1000, 10)?;
if utxo_set.process_transaction(&tx) {
println!("✓ UTXO updated successfully");
} else {
println!("✗ Invalid transaction");
}
}
Query Operations
find_utxos
#![allow(unused)]
fn main() {
pub fn find_utxos(&self, address: &str) -> Vec<(String, usize, u64)>
}
Finds all UTXOs for an address.
Return format: Vec<(txid, vout, value)>
Parameters:
address- The address to query
Return value: List of UTXOs
Example:
#![allow(unused)]
fn main() {
let utxos = utxo_set.find_utxos(&alice.address);
println!("Alice's UTXOs:");
for (txid, vout, value) in utxos {
println!(" {}:{} → {} sat", &txid[..8], vout, value);
}
// Output:
// Alice's UTXOs:
// tx1:0 → 5000 sat
// tx2:1 → 3000 sat
// tx5:0 → 2000 sat
}
find_spendable_outputs
#![allow(unused)]
fn main() {
pub fn find_spendable_outputs(
&self,
address: &str,
amount: u64
) -> Option<(u64, Vec<(String, usize)>)>
}
Finds a combination of UTXOs usable for payment.
UTXO selection strategies:
-
Greedy algorithm (current implementation):
#![allow(unused)] fn main() { accumulated = 0 for utxo in utxos: accumulated += utxo.value if accumulated >= amount: return utxos } -
Optimal match (possible optimization):
- Select the combination closest in total to the target
- Reduces change, saving on fees
-
Smallest UTXO first:
- Prefer small UTXOs
- Avoids UTXO fragmentation
Parameters:
address- Sender addressamount- Required amount (including fee)
Return value:
Some((accumulated, utxo_list))- Sufficient UTXOs foundaccumulated: Accumulated amountutxo_list: Selected UTXO list
None- Insufficient balance
Example:
#![allow(unused)]
fn main() {
// Need 1000 sat (including fee)
let result = utxo_set.find_spendable_outputs(&alice.address, 1000);
match result {
Some((accumulated, utxos)) => {
println!("✓ Found sufficient UTXOs");
println!(" Accumulated amount: {} sat", accumulated);
println!(" UTXOs used: {}", utxos.len());
println!(" Change: {} sat", accumulated - 1000);
}
None => {
println!("✗ Insufficient balance");
}
}
}
get_balance
#![allow(unused)]
fn main() {
pub fn get_balance(&self, address: &str) -> u64
}
Calculates the total balance for an address.
Computation method:
#![allow(unused)]
fn main() {
balance = sum(all_utxos.value)
}
Parameters:
address- The address to query
Return value: Balance (satoshi)
Example:
#![allow(unused)]
fn main() {
let balance = utxo_set.get_balance(&alice.address);
println!("Balance: {} satoshi", balance);
println!("Balance: {:.8} BTC", balance as f64 / 100_000_000.0);
// Batch query
let addresses = vec![alice.address, bob.address, charlie.address];
for addr in addresses {
let bal = utxo_set.get_balance(&addr);
println!("{}: {} sat", &addr[..10], bal);
}
}
Use Cases
Case 1: Selecting UTXOs When Creating a Transaction
#![allow(unused)]
fn main() {
fn create_payment(
utxo_set: &UTXOSet,
from: &Wallet,
to: &str,
amount: u64,
fee: u64
) -> Result<Transaction, String> {
let total_needed = amount + fee;
// 1. Find available UTXOs
let result = utxo_set.find_spendable_outputs(&from.address, total_needed);
let (accumulated, utxo_refs) = result.ok_or("Insufficient balance")?;
// 2. Build inputs
let mut inputs = Vec::new();
for (txid, vout) in utxo_refs {
let signature = from.sign(&format!("{}{}", txid, vout));
inputs.push(TxInput::new(txid, vout, signature, from.public_key.clone()));
}
// 3. Build outputs
let mut outputs = vec![
TxOutput::new(amount, to.to_string()), // To recipient
];
// 4. Change
if accumulated > total_needed {
outputs.push(TxOutput::new(
accumulated - total_needed,
from.address.clone()
));
}
// 5. Create transaction
Ok(Transaction::new(inputs, outputs, current_timestamp(), fee))
}
}
Case 2: Detailed Balance Query
#![allow(unused)]
fn main() {
fn balance_breakdown(utxo_set: &UTXOSet, address: &str) {
let utxos = utxo_set.find_utxos(address);
let total = utxo_set.get_balance(address);
println!("=== Balance Details ===");
println!("Address: {}", &address[..20]);
println!("Total balance: {} sat ({:.8} BTC)", total, total as f64 / 1e8);
println!("UTXO count: {}", utxos.len());
println!("\nUTXO list:");
for (i, (txid, vout, value)) in utxos.iter().enumerate() {
println!(" #{}: {}:{} → {} sat",
i + 1, &txid[..8], vout, value);
}
// Statistics
if !utxos.is_empty() {
let avg = total / utxos.len() as u64;
let max = utxos.iter().map(|(_, _, v)| v).max().unwrap();
let min = utxos.iter().map(|(_, _, v)| v).min().unwrap();
println!("\nStatistics:");
println!(" Average: {} sat", avg);
println!(" Maximum: {} sat", max);
println!(" Minimum: {} sat", min);
}
}
}
Case 3: UTXO Consolidation
#![allow(unused)]
fn main() {
fn consolidate_utxos(
blockchain: &mut Blockchain,
wallet: &Wallet
) -> Result<(), String> {
let utxos = blockchain.utxo_set.find_utxos(&wallet.address);
// If too many UTXOs (>50), consolidate into 1
if utxos.len() > 50 {
println!("Starting UTXO consolidation...");
println!(" Current UTXO count: {}", utxos.len());
// Create a self-to-self transaction consolidating all UTXOs
let total = blockchain.get_balance(&wallet.address);
let fee = 100; // Fixed fee
let tx = blockchain.create_transaction(
wallet,
wallet.address.clone(),
total - fee,
fee,
)?;
blockchain.add_transaction(tx)?;
blockchain.mine_pending_transactions(wallet.address.clone())?;
let new_utxos = blockchain.utxo_set.find_utxos(&wallet.address);
println!("✓ Consolidation complete");
println!(" New UTXO count: {}", new_utxos.len());
}
Ok(())
}
}
Case 4: UTXO Audit
#![allow(unused)]
fn main() {
fn audit_utxo_set(utxo_set: &UTXOSet, blockchain: &Blockchain) -> bool {
println!("=== UTXO Audit ===");
// 1. Count total UTXOs
let total_utxos: usize = utxo_set.utxos.values()
.map(|v| v.len())
.sum();
println!("Total UTXO count: {}", total_utxos);
// 2. Sum total value
let mut total_value = 0u64;
for outputs in utxo_set.utxos.values() {
for (_, output) in outputs {
total_value += output.value;
}
}
println!("Total value: {} sat", total_value);
// 3. Validate each UTXO
let mut valid = true;
for (txid, outputs) in &utxo_set.utxos {
// Verify the transaction exists on the blockchain
let tx_exists = blockchain.chain.iter()
.any(|block| block.transactions.iter()
.any(|tx| &tx.id == txid));
if !tx_exists {
println!("✗ Warning: UTXO references non-existent transaction {}", txid);
valid = false;
}
}
if valid {
println!("✓ UTXO set is complete");
}
valid
}
}
Performance Optimization
1. Index Optimization
#![allow(unused)]
fn main() {
// Create an index for addresses
pub struct IndexedUTXOSet {
utxos: HashMap<String, Vec<(usize, TxOutput)>>,
// New: address index
address_index: HashMap<String, Vec<(String, usize)>>,
}
impl IndexedUTXOSet {
pub fn find_utxos(&self, address: &str) -> Vec<(String, usize, u64)> {
// O(1) lookup instead of O(n)
if let Some(refs) = self.address_index.get(address) {
refs.iter()
.filter_map(|(txid, vout)| {
self.utxos.get(txid)
.and_then(|outputs| outputs.iter()
.find(|(idx, _)| idx == vout)
.map(|(_, output)| (txid.clone(), *vout, output.value))
)
})
.collect()
} else {
vec![]
}
}
}
}
2. Batch Operations
#![allow(unused)]
fn main() {
// Process transactions in batch
pub fn process_transactions(&mut self, txs: &[Transaction]) -> bool {
// 1. Validate all transactions
for tx in txs {
if !tx.verify() {
return false;
}
}
// 2. Batch update UTXOs
for tx in txs {
// Remove inputs
if !tx.is_coinbase() {
for input in &tx.inputs {
self.remove_utxo(&input.txid, input.vout);
}
}
// Add outputs
self.add_transaction(tx);
}
true
}
}
3. Balance Caching
#![allow(unused)]
fn main() {
pub struct CachedUTXOSet {
utxos: HashMap<String, Vec<(usize, TxOutput)>>,
balance_cache: HashMap<String, u64>, // Balance cache
}
impl CachedUTXOSet {
pub fn get_balance(&mut self, address: &str) -> u64 {
// Check cache
if let Some(balance) = self.balance_cache.get(address) {
return *balance;
}
// Compute and cache
let balance = self.calculate_balance(address);
self.balance_cache.insert(address.to_string(), balance);
balance
}
fn invalidate_cache(&mut self, address: &str) {
self.balance_cache.remove(address);
}
}
}
Comparison with Ethereum Account Model
| Feature | UTXO Model (Bitcoin) | Account Model (Ethereum) |
|---|---|---|
| State | Stateless (only UTXO set) | Stateful (account balances, nonce) |
| Balance | Computed value (UTXO sum) | Stored value (directly stored) |
| Transfer | Spend UTXOs, create new UTXOs | Account balance increases/decreases |
| Privacy | Better (can use new address) | Worse (reuse of addresses) |
| Parallelism | Easy to validate in parallel | Requires sequential processing (nonce) |
| Complexity | Complex transaction construction | Simple transactions |
| Smart contracts | Limited (Script) | Flexible (EVM) |
Frequently Asked Questions
Q: Why use the UTXO model?
A:
- ✅ Better privacy (new address each time)
- ✅ Parallel validation (different UTXOs are independent)
- ✅ Simplified validation logic
- ✅ Natural double-spend prevention
Q: Will UTXOs keep accumulating?
A: Yes. Solutions:
- UTXO consolidation (merge multiple small UTXOs)
- Higher transaction fees (limit junk UTXOs)
- UTXO commitments (reduce storage)
Q: How do I prevent UTXO fragmentation?
A:
#![allow(unused)]
fn main() {
// Consolidate periodically
if utxos.len() > threshold {
consolidate_utxos();
}
// Prioritize small UTXOs
utxos.sort_by_key(|u| u.value); // Smallest first
}
Q: What if UTXOs are lost?
A: As long as you have the private key, you can rebuild the UTXO set from the blockchain:
#![allow(unused)]
fn main() {
fn rebuild_utxo_set(blockchain: &Blockchain, address: &str) -> UTXOSet {
let mut utxo_set = UTXOSet::new();
for block in &blockchain.chain {
for tx in &block.transactions {
utxo_set.process_transaction(tx);
}
}
utxo_set
}
}
References
- Transaction API - Creating and spending UTXOs
- Blockchain API - UTXO set management
- Basic Concepts - UTXO Model
- Bitcoin Whitepaper