UTXO vs Account Model

Blockchains use two fundamentally different approaches to track ownership: the UTXO model (Bitcoin) and the Account model (Ethereum, Solana). Understanding both helps you appreciate Solana's design decisions.

The Mental Models

UTXO: Digital Cash

Think of UTXOs as physical bills in your wallet:

Text
Alice's Wallet:
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│   $20 bill  │  │   $10 bill  │  │   $5 bill   │
│ (can spend) │  │ (can spend) │  │ (can spend) │
└─────────────┘  └─────────────┘  └─────────────┘

Alice's "balance" = $20 + $10 + $5 = $35

To pay Bob $25:
- Alice gives the $20 bill (destroyed)
- Alice gives the $10 bill (destroyed)
- Bob receives a new $25 bill (created)
- Alice receives a new $5 bill as change (created)

Account: Bank Balance

Think of accounts like a bank ledger:

Text
Bank Ledger:
┌─────────────────────────────┐
│ Alice:  $35.00              │
│ Bob:    $50.00              │
│ Carol:  $100.00             │
└─────────────────────────────┘

To pay Bob $25:
- Alice's balance: $35 → $10
- Bob's balance:   $50$75

UTXO Model Deep Dive

UTXO Structure

TypeScript
interface UTXO {
  // Unique identifier
  txid: string; // Transaction that created this UTXO
  vout: number; // Output index within that transaction

  // Value
  value: number; // Amount in smallest unit (satoshis)

  // Ownership
  scriptPubKey: string; // Conditions to spend (locking script)

  // State
  spent: boolean; // Has this been consumed?
}

How Transactions Work

Text
Transaction: Alice pays Bob 0.5 BTC

INPUTS (UTXOs being consumed):
┌────────────────────────────────────────┐
│ UTXO #1                                │
│ txid: abc123, vout: 0                  │
│ value: 0.7 BTC                         │
│ scriptPubKey: "Alice can spend"        │
│ scriptSig: "Alice's signature"         │ ← Proof Alice owns it
└────────────────────────────────────────┘

OUTPUTS (New UTXOs created):
┌────────────────────────────────────────┐
│ UTXO #2                                │
│ value: 0.5 BTC                         │
│ scriptPubKey: "Bob can spend"          │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ UTXO #3 (change)                       │
│ value: 0.199 BTC                       │
│ scriptPubKey: "Alice can spend"        │
└────────────────────────────────────────┘

Fee = 0.7 - 0.5 - 0.199 = 0.001 BTC

UTXO Code Example

TypeScript
interface UTXOInput {
  txid: string;
  vout: number;
  scriptSig: Buffer; // Unlocking script (signature)
}

interface UTXOOutput {
  value: number;
  scriptPubKey: Buffer; // Locking script
}

interface UTXOTransaction {
  version: number;
  inputs: UTXOInput[];
  outputs: UTXOOutput[];
  lockTime: number;
}

function validateUTXOTransaction(
  tx: UTXOTransaction,
  utxoSet: Map<string, UTXO>,
): boolean {
  let inputSum = 0;
  let outputSum = 0;

  // Validate each input
  for (const input of tx.inputs) {
    const utxoKey = `${input.txid}:${input.vout}`;
    const utxo = utxoSet.get(utxoKey);

    // UTXO must exist and be unspent
    if (!utxo || utxo.spent) {
      return false;
    }

    // Verify signature (simplified)
    if (!verifyScript(input.scriptSig, utxo.scriptPubKey, tx)) {
      return false;
    }

    inputSum += utxo.value;
  }

  // Sum outputs
  for (const output of tx.outputs) {
    if (output.value < 0) return false;
    outputSum += output.value;
  }

  // Inputs must cover outputs (difference is fee)
  return inputSum >= outputSum;
}

UTXO Advantages

  1. Parallelism: Different UTXOs can be spent in parallel
  2. Privacy: Fresh addresses for each transaction
  3. Simple validation: Each UTXO validated independently
  4. Auditability: Easy to trace coin history
  5. No state bloat: Spent UTXOs can be pruned

UTXO Disadvantages

  1. Complex change handling: Must create change outputs
  2. Coin selection: Algorithm needed to choose which UTXOs to spend
  3. Limited programmability: Scripts are restricted
  4. State explosion: Many small UTXOs ("dust")

Account Model Deep Dive

Account Structure

TypeScript
// Ethereum-style account
interface Account {
  address: string;
  balance: bigint; // In wei (smallest unit)
  nonce: number; // Prevents replay attacks
  codeHash: string; // Empty for EOA, bytecode hash for contracts
  storageRoot: string; // Merkle root of storage (contracts only)
}

// Solana-style account
interface SolanaAccount {
  pubkey: PublicKey;
  lamports: bigint; // Balance in lamports
  owner: PublicKey; // Program that owns this account
  executable: boolean; // Is this a program?
  rentEpoch: number; // Last rent payment
  data: Uint8Array; // Arbitrary data
}

How Transactions Work

Text
Transaction: Alice pays Bob 0.5 ETH

Before:
┌──────────────────────┐  ┌──────────────────────┐
│ Alice                │  │ Bob                  │
│ balance: 1.0 ETH     │  │ balance: 0.5 ETH     │
│ nonce: 5             │  │ nonce: 2             │
└──────────────────────┘  └──────────────────────┘

Transaction:
{
  from: Alice,
  to: Bob,
  value: 0.5 ETH,
  nonce: 5,           // Must match Alice's current nonce
  gasPrice: 20 gwei,
  signature: "..."
}

After:
┌──────────────────────┐  ┌──────────────────────┐
│ Alice                │  │ Bob                  │
│ balance: 0.499 ETH   │  │ balance: 1.0 ETH     │
│ nonce: 6             │  │ nonce: 2             │
└──────────────────────┘  └──────────────────────┘

(0.001 ETH paid as gas fee)

Account Model Code Example

TypeScript
interface AccountTransaction {
  from: string;
  to: string;
  value: bigint;
  nonce: number;
  data: Buffer; // For contract calls
  gasLimit: number;
  gasPrice: bigint;
  signature: Buffer;
}

function validateAccountTransaction(
  tx: AccountTransaction,
  state: Map<string, Account>,
): boolean {
  const sender = state.get(tx.from);
  if (!sender) return false;

  // Verify nonce
  if (tx.nonce !== sender.nonce) {
    return false; // Replay protection
  }

  // Calculate total cost
  const gasCost = BigInt(tx.gasLimit) * tx.gasPrice;
  const totalCost = tx.value + gasCost;

  // Check balance
  if (sender.balance < totalCost) {
    return false;
  }

  // Verify signature
  if (!verifySignature(tx)) {
    return false;
  }

  return true;
}

function executeAccountTransaction(
  tx: AccountTransaction,
  state: Map<string, Account>,
): void {
  const sender = state.get(tx.from)!;
  let recipient = state.get(tx.to);

  // Create recipient if doesn't exist
  if (!recipient) {
    recipient = {
      address: tx.to,
      balance: 0n,
      nonce: 0,
      codeHash: "",
      storageRoot: "",
    };
    state.set(tx.to, recipient);
  }

  // Update balances
  const gasCost = BigInt(tx.gasLimit) * tx.gasPrice;
  sender.balance -= tx.value + gasCost;
  sender.nonce += 1;
  recipient.balance += tx.value;
}

Account Model Advantages

  1. Simplicity: No change outputs, just balance updates
  2. Programmability: Rich smart contract support
  3. Fungibility: All balance is equivalent (no UTXO selection)
  4. Space efficiency: One entry per account

Account Model Disadvantages

  1. Sequential processing: Same account = sequential (in naive implementations)
  2. State bloat: Accounts persist even with zero balance
  3. Privacy: Address reuse common
  4. Replay attacks: Need nonces to prevent

Solana's Hybrid Approach

Solana uses an account model but borrows ideas from UTXO:

Key Innovation: Account Declaration

Text
Traditional Account Model (Ethereum):
┌───────────────────────────────────────┐
│ Transaction calls contract            │
│ Contract reads/writes storage         │
│ Runtime discovers state access        │
└───────────────────────────────────────┘
Result: Must execute sequentially (might conflict)

Solana's Model:
┌───────────────────────────────────────┐
│ Transaction declares ALL accounts     │
│ Runtime knows full state footprint    │
│ Non-conflicting txs run in parallel   │
└───────────────────────────────────────┘
Result: UTXO-like parallelism with account simplicity

Solana Account Declaration

TypeScript
// Every Solana instruction explicitly lists accounts
interface Instruction {
  programId: PublicKey;
  keys: AccountMeta[]; // ALL accounts must be listed
  data: Buffer;
}

interface AccountMeta {
  pubkey: PublicKey;
  isSigner: boolean; // Must this account sign?
  isWritable: boolean; // Will this account be modified?
}

// Example: Token transfer
const transferInstruction = {
  programId: TOKEN_PROGRAM_ID,
  keys: [
    { pubkey: sourceAccount, isSigner: false, isWritable: true },
    { pubkey: destAccount, isSigner: false, isWritable: true },
    { pubkey: ownerAccount, isSigner: true, isWritable: false },
  ],
  data: encodeTransferData(amount),
};

Why This Matters for Performance

Text
Incoming transactions:
Tx1: [Account A (write), Account B (read)]
Tx2: [Account C (write), Account D (read)]
Tx3: [Account A (read), Account E (write)]

Solana's scheduler sees:
- Tx1 and Tx2 have NO overlap → Run in parallel
- Tx3 reads A, Tx1 writes A → Tx3 must wait for Tx1

Processing timeline:
Time ─────────────────────────────────►
     ┌──────────┐
Tx1  │ Execute  │
     └──────────┘
     ┌──────────┐
Tx2  │ Execute  │  (parallel with Tx1)
     └──────────┘
                 ┌──────────┐
Tx3              │ Execute  │  (after Tx1)
                 └──────────┘

Practical Comparison

Scenario: Payment Processing

Text
Process 1000 payments:

UTXO (Bitcoin):
- Each payment is independent
- Can process in parallel (if UTXOs don't overlap)
- Need coin selection for each payment
- Generate 1000 change outputs

Account (Ethereum):
- All from same account
- MUST process sequentially (same nonce)
- Simple balance updates
- No change management

Solana:
- Different source accounts = parallel
- Same source account = sequential
- Batching reduces transaction count

Scenario: Token Swap

Text
Swap Token A for Token B:

UTXO approach would need:
- Complex scripts for atomic swaps
- Multiple transactions
- Time locks

Account approach:
- Single contract call
- Atomic execution
- Simple programming model

Scenario: High-Frequency Trading

Text
Many independent traders, same market:

Ethereum:
- All touch same contract
- Sequential execution
- Bottleneck

Solana:
- Market state in separate accounts
- Orders touching different price levels = parallel
- Much higher throughput

State Management Comparison

AspectUTXOAccount (Ethereum)Account (Solana)
State LocationUTXOsContract storageSeparate accounts
State SizeSum of UTXOsMapping slotsAccount data
PruningSpent UTXOsDifficultRent mechanism
Access PatternExplicit by UTXODiscovered at runtimeDeclared upfront
ParallelismNaturalSequentialDeclared parallelism

Key Takeaways

  1. UTXO treats value as discrete "coins"—good for parallelism, tricky for programmability
  2. Account model uses balance ledgers—simpler programming, sequential by default
  3. Solana combines account simplicity with UTXO-inspired parallelism via upfront declaration
  4. No model is universally better—each optimizes for different use cases

Common Mistakes

  1. Assuming all account models are the same: Solana's account model differs significantly from Ethereum's
  2. Ignoring write conflicts: Two transactions writing the same Solana account must be sequential
  3. Over-batching: Sometimes separate transactions parallelize better than one big batch

Try It Yourself

  1. Trace a Bitcoin transaction: On a block explorer, find the input UTXOs and output UTXOs. Calculate the fee.

  2. Analyze Solana parallelism: For these accounts, which transactions can run in parallel?

    • Tx1: Write A, Read B
    • Tx2: Write C, Read D
    • Tx3: Read A, Write E
    • Tx4: Write B, Write C
  3. Design an account structure: For a simple game with player scores, how would you structure accounts to maximize parallelism?


Next: Consensus Mechanisms - How blockchains agree on the state of the world.