Blocks & Transactions

Difficulty: Beginner

A block is the fundamental unit of data in a blockchain. Each block contains two main parts: the block header and the block body. The block header contains metadata - the previous block's hash, a timestamp, the difficulty target, a nonce (in PoW chains), and the Merkle root of all transactions in the block. The block body contains the actual list of transactions. This separation is important because light clients can verify block headers without downloading the full transaction data.

The Merkle tree (or hash tree) is a data structure that efficiently summarizes all transactions in a block. It works by hashing each transaction individually (the leaf nodes), then pairing adjacent hashes and hashing them together, repeating this process up the tree until a single hash remains - the Merkle root. This root is stored in the block header. The Merkle tree enables Simplified Payment Verification (SPV): to prove a transaction is included in a block, you only need the transaction hash plus a small set of intermediate hashes (the Merkle proof), not the entire block. This makes verification logarithmic in complexity rather than linear.

The transaction pool (also called the mempool) is where unconfirmed transactions wait before being included in a block. When a user broadcasts a transaction, it propagates through the P2P network and lands in each node's mempool. Miners or validators select transactions from the mempool to include in the next block, typically prioritizing those with higher fees. Transactions remain in the mempool until they are either included in a block or dropped due to expiration, low fees, or mempool size limits.

Bitcoin uses the UTXO (Unspent Transaction Output) model for tracking balances. Instead of maintaining account balances like a bank, Bitcoin tracks individual unspent outputs from previous transactions. When Alice sends Bob 1 BTC, she references a previous transaction output she received, consumes it entirely, and creates new outputs - one paying Bob and potentially one returning change to herself. This model provides strong privacy (each transaction can use fresh addresses) and enables parallel transaction validation. Ethereum, by contrast, uses an account-based model where each address has a balance that is directly debited and credited.

Understanding the lifecycle of a transaction is essential. A transaction is created and signed by the sender, broadcast to the network, validated by nodes (checking signature, sufficient balance, correct nonce), held in the mempool, selected by a block producer, included in a block, and finally confirmed as subsequent blocks are added on top. The number of confirmations indicates how deeply buried a transaction is in the chain, which correlates with its security against reversal.

Code examples

Merkle Tree Construction in JavaScript

const crypto = require('crypto');

function sha256(data) {
  return crypto.createHash('sha256').update(data).digest('hex');
}

class MerkleTree {
  constructor(transactions) {
    this.leaves = transactions.map(tx => sha256(tx));
    this.layers = [this.leaves];
    this.root = this.buildTree();
  }

  buildTree() {
    let currentLayer = this.leaves;

    while (currentLayer.length > 1) {
      const nextLayer = [];

      // If odd number of nodes, duplicate the last one
      if (currentLayer.length % 2 !== 0) {
        currentLayer.push(currentLayer[currentLayer.length - 1]);
      }

      for (let i = 0; i < currentLayer.length; i += 2) {
        const combinedHash = sha256(currentLayer[i] + currentLayer[i + 1]);
        nextLayer.push(combinedHash);
      }

      this.layers.push(nextLayer);
      currentLayer = nextLayer;
    }

    return currentLayer[0];
  }

  getProof(index) {
    const proof = [];
    let currentIndex = index;

    for (let i = 0; i < this.layers.length - 1; i++) {
      const layer = this.layers[i];
      const isRightNode = currentIndex % 2 === 1;
      const siblingIndex = isRightNode ? currentIndex - 1 : currentIndex + 1;

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

      currentIndex = Math.floor(currentIndex / 2);
    }

    return proof;
  }

  static verify(transactionHash, proof, root) {
    let computedHash = transactionHash;

    for (const step of proof) {
      if (step.position === 'left') {
        computedHash = sha256(step.hash + computedHash);
      } else {
        computedHash = sha256(computedHash + step.hash);
      }
    }

    return computedHash === root;
  }
}

// Example usage
const transactions = [
  'Alice -> Bob: 5 BTC',
  'Bob -> Charlie: 2 BTC',
  'Charlie -> Dave: 1 BTC',
  'Eve -> Frank: 3 BTC',
];

const tree = new MerkleTree(transactions);
console.log('Merkle Root:', tree.root);
console.log('Number of layers:', tree.layers.length);

// Generate and verify a proof for the second transaction
const txIndex = 1;
const txHash = sha256(transactions[txIndex]);
const proof = tree.getProof(txIndex);
console.log('\nProof for transaction:', transactions[txIndex]);
console.log('Proof steps:', proof.length);

const isValid = MerkleTree.verify(txHash, proof, tree.root);
console.log('Proof valid:', isValid);

This Merkle tree implementation demonstrates how transactions are hashed into a tree structure. The root hash summarizes all transactions in a single 256-bit value stored in the block header. The getProof method generates a Merkle proof - the minimal set of sibling hashes needed to reconstruct the path from a leaf to the root. The verify method shows that you can confirm a transaction's inclusion using only the transaction hash, the proof, and the root - without needing all other transactions.

Key points

Concepts covered

Block Header, Merkle Tree, Transaction Pool, UTXO