Blocks and Transactions

Blocks and transactions are the fundamental data structures of every blockchain. Understanding their anatomy helps you reason about performance, security, and costs.

What Is a Transaction?

A transaction is a signed message that requests a state change on the blockchain.

Anatomy of a Transaction

Every blockchain transaction contains:

Text
┌─────────────────────────────────────────────────────┐
│                    Transaction                       │
├─────────────────────────────────────────────────────┤
│  Sender          Who's requesting this action?       │
│  Recipient       Where's it going?                   │
│  Value           How much to transfer?               │
│  Data            What operation to perform?          │
│  Nonce           Prevents replay attacks             │
│  Fee Info        How much to pay for processing?     │
│  Signature       Proves sender authorized this       │
└─────────────────────────────────────────────────────┘

Transaction Lifecycle

Text
1. CREATION
   User constructs transaction with intended action

2. SIGNING
   User signs with private key, proving authorization

3. BROADCAST
   Transaction sent to network nodes

4. MEMPOOL
   Nodes validate and store in pending pool

5. INCLUSION
   Validator/miner includes in a block

6. EXECUTION
   Transaction runs, state changes applied

7. CONFIRMATION
   Block added to chain, transaction finalized

Solana Transaction Structure

Solana transactions have a unique structure optimized for parallel processing:

TypeScript
interface SolanaTransaction {
  // Message contains the actual instructions
  message: {
    // Header with signature and account counts
    header: {
      numRequiredSignatures: number;
      numReadonlySignedAccounts: number;
      numReadonlyUnsignedAccounts: number;
    };

    // All accounts this transaction will touch
    accountKeys: PublicKey[];

    // Recent blockhash (prevents replay, sets expiry)
    recentBlockhash: string;

    // Instructions to execute
    instructions: CompiledInstruction[];
  };

  // Signatures from required signers
  signatures: Signature[];
}

interface CompiledInstruction {
  // Index into accountKeys for the program to invoke
  programIdIndex: number;

  // Indices into accountKeys for accounts to pass
  accountKeyIndexes: number[];

  // Arbitrary data interpreted by the program
  data: Uint8Array;
}

Why Solana Declares Accounts Upfront

This is crucial for parallelism:

Text
Traditional (Ethereum):
Transaction says: "Call contract X"
Runtime discovers: "X reads accounts A, B, writes C"
Problem: Can't know conflicts until execution

Solana:
Transaction says: "Use accounts A, B, C; call program X"
Runtime knows: "This transaction touches A, B, C"
Benefit: Can schedule non-conflicting transactions in parallel

Example:
Tx1: Touches [A, B] →
Tx2: Touches [C, D] → Can run simultaneously
Tx3: Touches [A, C] → Must wait for Tx1 and Tx2

Transaction Code Example

TypeScript
import {
  Connection,
  PublicKey,
  Transaction,
  TransactionInstruction,
  sendAndConfirmTransaction,
  Keypair,
  SystemProgram,
} from "@solana/web3.js";

// Create a simple SOL transfer transaction
async function createTransferTransaction(
  fromPubkey: PublicKey,
  toPubkey: PublicKey,
  lamports: number,
  connection: Connection,
): Promise<Transaction> {
  // Create the transaction
  const transaction = new Transaction();

  // Add a transfer instruction
  transaction.add(
    SystemProgram.transfer({
      fromPubkey,
      toPubkey,
      lamports,
    }),
  );

  // Get recent blockhash (required for transaction validity)
  const { blockhash, lastValidBlockHeight } =
    await connection.getLatestBlockhash();

  transaction.recentBlockhash = blockhash;
  transaction.lastValidBlockHeight = lastValidBlockHeight;
  transaction.feePayer = fromPubkey;

  return transaction;
}

// Send the transaction
async function sendTransfer(
  connection: Connection,
  payer: Keypair,
  recipient: PublicKey,
  amount: number,
): Promise<string> {
  const transaction = await createTransferTransaction(
    payer.publicKey,
    recipient,
    amount,
    connection,
  );

  // Sign and send
  const signature = await sendAndConfirmTransaction(
    connection,
    transaction,
    [payer], // Signers array
  );

  return signature;
}

What Is a Block?

A block is a batch of transactions bundled together with metadata that links it to previous blocks.

Block Structure

Text
┌──────────────────────────────────────────────────────┐
│                      Block Header                     │
├──────────────────────────────────────────────────────┤
│  Previous Hash    Link to parent block               │
│  Timestamp        When block was created             │
│  Merkle Root      Cryptographic summary of all txs   │
│  State Root       Hash of resulting state            │
│  Nonce/PoH        Consensus-specific data            │
├──────────────────────────────────────────────────────┤
│                    Block Body                         │
├──────────────────────────────────────────────────────┤
│  Transaction 1                                        │
│  Transaction 2                                        │
│  Transaction 3                                        │
│  ...                                                  │
│  Transaction N                                        │
└──────────────────────────────────────────────────────┘

Block Chaining

Each block references the previous block's hash, forming a chain:

Text
Block 0 (Genesis)     Block 1              Block 2
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│ prev: 0x000 │←─────│ prev: hash0 │←─────│ prev: hash1 │
│ data: ...   │      │ data: ...   │      │ data: ...   │
│ hash: hash0 │      │ hash: hash1 │      │ hash: hash2 │
└─────────────┘      └─────────────┘      └─────────────┘

Modifying Block 1 changes hash1
→ Block 2's prev_hash no longer matches
→ Chain is broken
→ Attack detected

Why Blocks Exist

Why not just chain individual transactions?

  1. Efficiency: Batching amortizes consensus overhead
  2. Atomicity: All transactions in a block are ordered
  3. Finality: Entire block is confirmed at once
  4. Merkle proofs: Efficiently prove transaction inclusion

Solana Block Structure

Solana has a unique block structure with slots and entries:

Text
Slot (Solana's "block")
┌────────────────────────────────────────────────────────┐
│  Slot Number: 123456789                                │
│  Parent Slot: 123456788                                │
│  Block Time: 1698765432                                │
│  Block Height: 98765432                                │
├────────────────────────────────────────────────────────┤
│  Entries:                                              │
│  ┌────────────────────────────────────────────────┐   │
│  │ Entry 1                                        │   │
│  │ - PoH Hash: 0xabc...                          │   │
│  │ - Transactions: [Tx1, Tx2, Tx3]               │   │
│  └────────────────────────────────────────────────┘   │
│  ┌────────────────────────────────────────────────┐   │
│  │ Entry 2                                        │   │
│  │ - PoH Hash: 0xdef...                          │   │
│  │ - Transactions: [Tx4, Tx5]                    │   │
│  └────────────────────────────────────────────────┘   │
│  ...                                                   │
└────────────────────────────────────────────────────────┘

Merkle Trees: Efficient Transaction Verification

A Merkle tree is a hash-based data structure that allows efficient proof of data inclusion.

Building a Merkle Tree

Text
Transactions:
[Tx0] [Tx1] [Tx2] [Tx3]
  │     │     │     │
  ▼     ▼     ▼     ▼
[H0]  [H1]  [H2]  [H3]Hash each transaction
  │     │     │     │
  └──┬──┘     └──┬──┘
     │           │
   [H01]       [H23]Hash pairs together
     │           │
     └─────┬─────┘
           │
       [Root]Merkle Root

H0 = hash(Tx0)
H1 = hash(Tx1)
H01 = hash(H0 + H1)
Root = hash(H01 + H23)

Merkle Proofs

To prove Tx1 is in the block, you only need:

Text
Proof for Tx1:
1. H0 (sibling at level 0)
2. H23 (sibling at level 1)

Verifier computes:
H1 = hash(Tx1)
H01 = hash(H0 + H1)
Root' = hash(H01 + H23)

If Root' == Root in block header → Tx1 is in block!

Proof size: O(log n) instead of O(n)
For 1 million transactions: ~20 hashes instead of 1 million

Merkle Tree Implementation

TypeScript
import { createHash } from "crypto";

function sha256(data: Buffer): Buffer {
  return createHash("sha256").update(data).digest();
}

function buildMerkleTree(transactions: Buffer[]): Buffer {
  if (transactions.length === 0) {
    return Buffer.alloc(32); // Empty tree
  }

  // Hash all transactions
  let level = transactions.map((tx) => sha256(tx));

  // Build tree bottom-up
  while (level.length > 1) {
    const nextLevel: Buffer[] = [];

    for (let i = 0; i < level.length; i += 2) {
      const left = level[i];
      // If odd number, duplicate last element
      const right = level[i + 1] ?? level[i];

      // Combine and hash
      const combined = Buffer.concat([left, right]);
      nextLevel.push(sha256(combined));
    }

    level = nextLevel;
  }

  return level[0]; // Root
}

interface MerkleProof {
  index: number;
  proof: Array<{ hash: Buffer; position: "left" | "right" }>;
}

function generateProof(
  transactions: Buffer[],
  targetIndex: number,
): MerkleProof {
  const proof: MerkleProof = { index: targetIndex, proof: [] };
  let level = transactions.map((tx) => sha256(tx));
  let index = targetIndex;

  while (level.length > 1) {
    const isRightNode = index % 2 === 1;
    const siblingIndex = isRightNode ? index - 1 : index + 1;

    if (siblingIndex < level.length) {
      proof.proof.push({
        hash: level[siblingIndex],
        position: isRightNode ? "left" : "right",
      });
    }

    // Move to next level
    const nextLevel: Buffer[] = [];
    for (let i = 0; i < level.length; i += 2) {
      const left = level[i];
      const right = level[i + 1] ?? level[i];
      nextLevel.push(sha256(Buffer.concat([left, right])));
    }

    level = nextLevel;
    index = Math.floor(index / 2);
  }

  return proof;
}

function verifyProof(
  transaction: Buffer,
  proof: MerkleProof,
  root: Buffer,
): boolean {
  let hash = sha256(transaction);

  for (const step of proof.proof) {
    const combined =
      step.position === "left"
        ? Buffer.concat([step.hash, hash])
        : Buffer.concat([hash, step.hash]);
    hash = sha256(combined);
  }

  return hash.equals(root);
}

Transaction Finality

Finality refers to when a transaction becomes irreversible.

Types of Finality

Text
Probabilistic Finality (Bitcoin, PoW Ethereum):
─────────────────────────────────────────────
Each confirmation reduces reversal probability exponentially.

Block 1: 50% chance of reversal (shallow)
Block 2: 25%
Block 3: 12.5%
Block 6: ~1.5%Generally "final"
Block 12: ~0.02%

Absolute Finality (Solana, PoS Ethereum):
───────────────────────────────────────────
Once 2/3 of validators vote, block is final.

Slot created → Validators vote → Supermajority reached → FINAL
     │                                                      │
     └──────────────── ~400ms - 12 seconds ─────────────────┘

Solana Finality

Solana has multiple confirmation levels:

TypeScript
// Commitment levels in Solana
type Commitment = "processed" | "confirmed" | "finalized";

// processed: Transaction has been processed by the node
// confirmed: Voted on by supermajority of stake
// finalized: 31+ confirmed blocks built on top

// Example usage
const signature = await connection.sendTransaction(transaction);

// Wait for different confirmation levels
await connection.confirmTransaction(signature, "processed");
console.log("Processed!"); // Fastest, but might be rolled back

await connection.confirmTransaction(signature, "confirmed");
console.log("Confirmed!"); // Very likely final

await connection.confirmTransaction(signature, "finalized");
console.log("Finalized!"); // Absolutely final

Block Size and Throughput

The Scalability Trilemma

Text
                    Decentralization/ \
                        /   \
                       /     \
                      /       \
                     /    ●    \      ← Pick any two
                    /   Current \
                   /   Blockchains\
                  /________________\
         Security ◄─────────────────► Scalability

Block Size Trade-offs

Text
Larger blocks:More transactions per block
✓ Higher throughput
✗ Longer propagation time
✗ More storage required
✗ Higher validator requirements → Less decentralization

Smaller blocks:Faster propagation
✓ Lower requirements
✓ More decentralization
✗ Lower throughput
✗ Higher fees during congestion

Throughput Comparison

Text
Blockchain      Block Time    Block Size    ~TPS
────────────────────────────────────────────────
Bitcoin         10 min        1-4 MB        7
Ethereum        12 sec        ~80 KB        30
Solana          400 ms        128 MB        65,000 (max)
                              (theoretical)

Solana achieves higher throughput through:

  1. Parallel execution (Sealevel)
  2. Proof of History (no waiting for consensus on time)
  3. Gulf Stream (transaction forwarding)
  4. Turbine (block propagation)

Deep Dive: Transaction Ordering

Why Order Matters

Text
Scenario: Two transactions arrive simultaneously
Tx1: Swap 100 USDC for SOL at current price
Tx2: Large trade that moves the price

Order A: [Tx1, Tx2]
- User gets expected price
- Fair execution

Order B: [Tx2, Tx1]
- Price moves before user's trade
- User gets worse price (slippage)

This enables MEV (Maximal Extractable Value)—validators can profit by reordering transactions.

Solana's Approach

Solana uses a leader schedule known in advance:

Text
Epoch N:
Slot 0-3:    Leader A
Slot 4-7:    Leader B
Slot 8-11:   Leader C
...

Transactions flow directly to the current leader via Gulf Stream,
reducing latency and making ordering more deterministic.

Key Takeaways

  1. Transactions are signed requests for state changes
  2. Blocks batch transactions with cryptographic links to previous blocks
  3. Merkle trees enable efficient verification without downloading everything
  4. Finality varies by blockchain—understand your chain's guarantees
  5. Solana requires upfront account declaration for parallel execution

Common Mistakes

  1. Not waiting for finality: Don't consider a transaction complete until sufficiently confirmed
  2. Ignoring reorgs: On probabilistic finality chains, recent blocks can be reverted
  3. Hardcoding block times: Block times are averages, not guarantees
  4. Ignoring MEV: On-chain order != submission order

Try It Yourself

  1. Explore block structure: Use Solana Explorer to examine a recent block. Identify the slot number, transactions, and parent slot.

  2. Calculate Merkle proof size: For a block with 4,096 transactions, how many hashes would a Merkle proof contain?

  3. Compare confirmation times: Send a transaction on devnet and measure time to "processed", "confirmed", and "finalized".


Next: UTXO vs Account Model - A deeper comparison of the two fundamental approaches to tracking blockchain state.