A Deep Dive into Building Your Own Rollup: RollupKit Architecture Explained

0xKaushik0xKaushik
8 min read

In recent years, rollups have become a popular approach to scaling blockchains. But building a rollup from scratch is no small feat — especially for those new to decentralized development. This is where RollupKit comes in. RollupKit is a simplified version of a Sovereign Rollup that offers developers a sandbox environment to understand the architecture and key components of rollups.

In this post, we’ll go over the different parts of the RollupKit architecture, including data availability, state derivation, the sequencer, and the wallet/frontend interface. Let’s unpack each component and see how they work together to form a lightweight, scalable rollup solution.

For those interested, the full code for RollupKit is available on GitHub. Here’s the link to the repository.

What is RollupKit?

RollupKit is a streamlined version of a Sovereign Rollup — a type of rollup that relies on Ethereum only for data availability and consensus, skipping the more complex verification mechanisms like fraud proofs or zero-knowledge proofs.

Unlike optimistic or zero-knowledge rollups, RollupKit doesn’t have a trust-minimized bridge with Ethereum. Instead, it focuses on providing data availability through Ethereum’s calldata, making it more accessible for developers to experiment with the core mechanics of rollups without getting into the weeds of cryptographic proofs.

Think of RollupKit as a DIY kit for building your own mini-blockchain that scales using Ethereum but doesn’t rely on it for complex validations. It’s perfect for understanding the building blocks of rollup technology.

Key Components of the RollupKit Architecture

The RollupKit project is composed of three main layers:

  1. Node / Backend: Manages data availability, state transitions, and database storage.

  2. Sequencer: Collects and posts transactions in batches to the Ethereum network.

  3. Wallet / Frontend: Provides a user interface for interacting with the rollup.

Each component plays a critical role in ensuring that the rollup functions smoothly and efficiently.

Node / Backend

The node or backend in BYOR handles several key processes, including data availability, state derivation, and storing the current state. Let’s take a closer look at each of these functionalities.

1. Data Availability

Data availability is crucial for rollups. In BYOR, this is achieved by posting transaction data as calldata to the Inputs contract on Ethereum. This contract’s purpose is to serve as a repository of data accessible by the rollup.

Imagine the Inputs contract as a public bulletin board. Anyone can post data to it, and anyone can read the data from it. This setup ensures that all participants in the rollup have access to the transaction history. Users or sequencers can send transactions to the contract, and once posted, the contract emits a BatchAppended event, making it easy to track new transactions.

Example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Inputs {
    event BatchAppended(address sender);    function appendBatch(bytes calldata _data) external {
        require(msg.sender == tx.origin); // Only EOA
        emit BatchAppended(msg.sender);
    }
}

When a user or sequencer calls the appendBatch function, a BatchAppended event is emitted. This event acts as a signal to the backend, telling it that new data is available for processing.

Real-World Analogy: “The Drop Box”

Think of the Inputs contract like a public drop box at a post office. Anyone can drop a letter (data) into it, and everyone knows new letters have been added by the notification (event) on the bulletin board. This ensures transparency and accessibility to all participants, who can then retrieve the data from this shared storage space.

2. State Derivation

State derivation in BYOR involves taking input data (transactions) and updating the rollup’s state. The process includes defining the initial state (genesis state), fetching data, and transitioning between states.

Genesis State

The genesis state is the starting point of the rollup. It’s defined in a JSON file, genesis.json, which maps each address to an initial balance. This setup resembles a pre-funded account system, where each user has an initial balance but no ability to mint new tokens.

{
    "0x1234...abcd": 1000,
    "0xabcd...1234": 500
}

In this example, the addresses are pre-funded with tokens as their starting balances. This file represents the state of the rollup at block 0, ensuring all nodes start with the same foundational data.

Data Fetching

The BatchDownloader.ts script continuously fetches new data from the Inputs contract. It listens to BatchAppended events and retrieves transaction data, which is then passed through the State Transition Function (STF) to update the rollup’s state.

State Transition Function (STF)

The State Transition Function is the heart of the rollup. It takes inputs (transactions) and generates a new state based on them. In BYOR, batches are ordered by their appearance on L1 Ethereum; there’s no additional state tracking to define the order, so the STF is simplified.

Example:

function executeTransaction(state, tx) {
    const fromAccount = state[tx.from] || { balance: 0, nonce: 0 };
    const toAccount = state[tx.to] || { balance: 0, nonce: 0 };
    fromAccount.balance -= tx.value;
    toAccount.balance += tx.value;
    fromAccount.nonce += 1;    state[tx.from] = fromAccount;
    state[tx.to] = toAccount;
}

Real-World Analogy: “Account Ledger”

Imagine a community ledger where everyone writes down transactions in sequence. Each new entry depends on the previous one, creating a sequential history of all transactions. The STF updates the ledger (state) as new transactions are added.

3. Storing the State

BYOR uses a database to store account information, such as balances and nonces, for each address. This data is stored in an accounts table, with the following schema:

CREATE TABLE accounts (
    address TEXT PRIMARY KEY NOT NULL,
    balance INTEGER DEFAULT 0 NOT NULL,
    nonce INTEGER DEFAULT 0 NOT NULL
);

If a new user interacts with the rollup and has no previous entry in the database, they’re assigned a default balance and nonce of zero.

Sequencer

The sequencer in BYOR is responsible for gathering transactions and submitting them to the Inputs contract in batches. BYOR’s sequencer is divided into two main parts: Mempool.ts and BatchPoster.ts.

Mempool

The Mempool acts as a waiting room for transactions. Here, transactions are organized by fee, ensuring that higher-fee transactions are prioritized when forming a batch. This incentivizes users to pay higher fees for faster processing.

Batch Poster

The Batch Poster creates a batch of transactions from the mempool and posts it to the data availability layer (Ethereum). This batch-posting mechanism reduces the number of submissions to Ethereum, saving on gas costs and improving scalability.

Client / Frontend

BYOR includes a simple wallet interface for users to interact with the rollup. Built using Next.js and WalletConnect, the wallet provides an easy way for users to send tokens, view balances, and check transaction statuses.

Key Features

  1. Token Transfers: Users can send tokens to other addresses within the rollup.

  2. Transaction Status: Users can monitor the status of transactions, whether in the mempool or already posted to Ethereum.

  3. Gas Fee Prioritization: The interface highlights the importance of gas fees, showing users how fees affect transaction processing speed.

Real-World Analogy: “Online Banking App”

Think of the BYOR wallet like an online banking app. Users can check their balances, transfer funds to others, and view the status of recent transactions.

Example Flow: Sending Tokens

  1. User Initiates Transfer: Alice decides to send 100 tokens to Bob. She enters Bob’s address and specifies a gas fee.

  2. Transaction Enters Mempool: The transaction is added to the mempool, where it’s sorted by fee. Alice’s higher fee puts her transaction near the top.

  3. Batch Poster Submits: The sequencer posts Alice’s transaction, along with others in the batch, to the Inputs contract on Ethereum.

  4. Backend Fetches Batch: The backend detects the new BatchAppended event and retrieves Alice’s transaction data.

  5. State Transition Function Updates: The STF processes Alice’s transaction, updating her balance and Bob’s balance in the rollup.

  6. Database Stores New State: The updated balances are saved in the accounts table, ensuring the new state is recorded.

In-Depth Code Highlights

Smart Contract

The Inputs.sol contract serves as the single point of data submission, emitting events that make it easy to query for new transaction batches.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Inputs {
    event BatchAppended(address sender);
    function appendBatch(bytes calldata) external {
        require(msg.sender == tx.origin);
        emit BatchAppended(msg.sender);
    }
}

Database Schema

BYOR uses a simple SQL database schema to store account information and transaction details.

accounts Table:

CREATE TABLE accounts (
    address TEXT PRIMARY KEY NOT NULL,
    balance INTEGER DEFAULT 0 NOT NULL,
    nonce INTEGER DEFAULT 0 NOT NULL
);

transactions Table:

CREATE TABLE transactions (
    id INTEGER,
    from TEXT NOT NULL,
    to TEXT NOT NULL,
    value INTEGER NOT NULL,
    nonce INTEGER NOT NULL,
    fee INTEGER NOT NULL,
    feeRecipient TEXT NOT NULL,
    l1SubmittedDate INTEGER NOT NULL,
    hash TEXT NOT NULL,
    PRIMARY KEY(from, nonce)
);

State Transition Function

The function executeTransaction in StateUpdater.ts updates balances and nonces for each transaction in the state.

const DEFAULT_ACCOUNT = { balance: 0, nonce: 0 };
function executeTransaction(state, tx, feeRecipient) {
    const fromAccount = getAccount(state, tx.from, DEFAULT_ACCOUNT);
    const toAccount = getAccount(state, tx.to, DEFAULT_ACCOUNT);    fromAccount.nonce = tx.nonce;
    fromAccount.balance -= tx.value;
    toAccount.balance += tx.value;    fromAccount.balance -= tx.fee;
    feeRecipient.balance += tx.fee;
}

Event Fetching

getNewStates fetches new BatchAppended events, extracting calldata, timestamps, and sender details, then packaging this data for further processing.

function getNewStates() {
    const lastBatchBlock = getLastBatchBlock();
    const events = getLogs(lastBatchBlock);
    const calldata = getCalldata(events);
    const timestamps = getTimestamps(events);
    const posters = getTransactionPosters(events);
    updateLastFetchedBlock(lastBatchBlock);
    return zip(posters, timestamps, calldata);
}

Mempool Fee Sorting

Transactions in the mempool are sorted by fees to prioritize higher-fee transactions.

function popNHighestFee(txPool, n) {
    txPool.sort((a, b) => b.fee - a.fee);
    return txPool.splice(0, n);
}

Limitations and Future Developments

While BYOR provides a foundational model for a Sovereign Rollup, it has limitations:

  • No Fraud Proofs: Unlike optimistic rollups, BYOR doesn’t include a mechanism to challenge invalid state transitions.

  • Limited to Data Availability: Ethereum is used only for storing data, without any form of dispute resolution.

Future improvements could involve adding fraud proofs, using zk-rollup techniques, or implementing gas-efficient data storage.

0
Subscribe to my newsletter

Read articles from 0xKaushik directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

0xKaushik
0xKaushik