Building TinyL1: A Minimal Layer 1 Blockchain in Rust

Building TinyL1: A Minimal Layer 1 Blockchain in Rust

A few weeks ago I decided to stop reading about blockchains and start building one from scratch. The result is TinyL1, a minimal layer-1 blockchain written in Rust. It is not production-ready, but it implements the core ideas that make real chains work: accounts, state roots, Merkle trees, a stack VM, and Byzantine Fault Tolerant consensus.

This post explains what I built, why each piece matters, and what I learned along the way.

What TinyL1 includes

flowchart TB
    subgraph cli["CLI"]
        sim["simulate"]
        transfer["transfer"]
        node["node"]
    end

    subgraph consensus["BFT Consensus"]
        propose["Propose"]
        prevote["PreVote"]
        precommit["PreCommit"]
        commit["Commit"]
        propose --> prevote --> precommit --> commit --> propose
    end

    subgraph exec["Execution"]
        vm["Stack VM"]
        gas["Gas Meter"]
    end

    subgraph data["State & Crypto"]
        state[(Accounts)]
        merkle[[Merkle Tree]]
        sha3[[SHA3-256]]
    end

    cli --> consensus
    consensus -->|transactions| exec
    exec -->|state delta| data
    merkle -.->|tx_root| consensus
    state -.->|state_root| consensus
    sha3 -.->|block_hash| consensus
  • Account-based state with nonce replay protection
  • Merkle tree for transaction roots and state commitment
  • Stack-based VM with 16 opcodes and gas metering
  • Tendermint-style consensus (Propose → PreVote → PreCommit → Commit)
  • Deterministic CLI simulator for local testing

Accounts and state transitions

The simplest part of any chain is the ledger. TinyL1 stores accounts in a HashMap keyed by a 20-byte address. Each account has a balance and a nonce. The nonce prevents replay attacks: a transaction must match the sender's current nonce, and after execution the nonce is incremented.

pub struct Account {
    pub address: [u8; 20],
    pub balance: u64,
    pub nonce: u64,
}

The State::apply method checks the nonce and balance, then transfers value from sender to receiver. This is the core state-transition function of the chain.

Merkle trees

Every block in TinyL1 has a tx_root: the Merkle root of all transactions in that block. A Merkle tree takes many items and compresses them into a single hash. If any transaction changes, the root changes, which makes tampering detectable.

flowchart BT
    A["A"]
    B["B"]
    C["C"]
    D["D"]
    hA["h(A)"]
    hB["h(B)"]
    hC["h(C)"]
    hD["h(D)"]
    hash1["h(hA + hB)"]
    hash2["h(hC + hD)"]
    root["Merkle Root\nh(hash1 + hash2)"]

    A --> hA
    B --> hB
    C --> hC
    D --> hD
    hA --> hash1
    hB --> hash1
    hC --> hash2
    hD --> hash2
    hash1 --> root
    hash2 --> root

Merkle proofs let you prove that a single transaction is part of a block without sending the whole block. That efficiency is why almost every blockchain uses them.

A stack VM

I also built a tiny stack-based virtual machine. It supports opcodes like Push, Add, Sub, Load, Store, Jump, JumpIfZero, and Halt. The VM has a gas counter: each instruction costs one unit of gas, and execution stops if the limit is exceeded.

The most satisfying test was a Fibonacci contract written entirely in bytecode:

let code = vec![
    Opcode::Push(0), Opcode::Store(0),
    Opcode::Push(1), Opcode::Store(1),
    Opcode::Push(10), Opcode::Store(2),
    Opcode::Load(1),
    Opcode::Load(0),
    Opcode::Add,
    Opcode::Store(1),
    Opcode::Load(1),
    Opcode::Load(0),
    Opcode::Sub,
    Opcode::Store(0),
    Opcode::Load(2),
    Opcode::Push(1),
    Opcode::Sub,
    Opcode::Dup(0),
    Opcode::Store(2),
    Opcode::JumpIfZero(23),
    Opcode::Jump(7),
    Opcode::Halt,
];

After ten iterations the VM holds mem[0] = 55 and mem[1] = 89. Writing bytecode by hand is tedious, but it made the VM design very concrete.

Consensus

Consensus is the protocol that lets a network agree on the next block. TinyL1 uses a simplified Tendermint-style round:

sequenceDiagram
    participant P as Proposer
    participant V1 as Validator 1
    participant V2 as Validator 2
    participant V3 as Validator 3

    P->>V1: Propose block B
    P->>V2: Propose block B
    P->>V3: Propose block B

    V1->>V1: PreVote(B)
    V2->>V2: PreVote(B)
    V3->>V3: PreVote(B)

    note over V1,V3: threshold reached

    V1->>V1: PreCommit(B)
    V2->>V2: PreCommit(B)
    V3->>V3: PreCommit(B)

    note over V1,V3: threshold reached → Commit

    V1->>V1: commit B
    V2->>V2: commit B
    V3->>V3: commit B

The two-phase vote prevents validators from accidentally committing different blocks at the same height. In TinyL1 a single ConsensusEngine tracks prevotes, precommits, and a threshold. Once the threshold is reached the block is finalized.

The CLI simulator

The project ships with a CLI built on clap. The most useful command is the transfer simulator:

RUST_LOG=info cargo run -- transfer --blocks 3 --transfers-per-block 5

It seeds an account with an initial balance, generates transfers, applies them to state, runs them through consensus, and prints committed blocks with live balance updates.

Committed block 1 hash=0x... tx_count=5 alice=950 bob=50
Committed block 2 hash=0x... tx_count=5 alice=900 bob=100
Committed block 3 hash=0x... tx_count=5 alice=850 bob=150

This made the whole pipeline feel real for the first time: transactions → state update → block proposal → consensus → committed chain.

What I learned

  1. Start small. A real blockchain has networking, P2P, mempool, signatures, and economic incentives. TinyL1 has none of that, but the core data structures and state machine are the same.
  2. Deterministic tests matter. Using real timestamps would make tests fragile. Passing timestamps explicitly kept everything reproducible.
  3. Rust borrow rules reveal design flaws. The first version of State::apply tried to hold two mutable references into the same HashMap. Refactoring it made the logic clearer.
  4. Mermaid diagrams are worth the effort. Drawing the architecture, consensus flow, and VM loop made the README much easier to understand.

What's next

  • Add Ed25519 transaction signing
  • Replace the serialized state hash with a real Merkle-Patricia trie
  • Add a peer-to-peer simulator so consensus runs across multiple processes
  • Build a tiny compiler or assembler for the VM so contracts are easier to write

The code

The full project is on GitHub at https://github.com/eyji-koike/rust-blockchain. If you want to see how a minimal layer-1 chain fits together, it is a good place to start.