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


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:
Node / Backend: Manages data availability, state transitions, and database storage.
Sequencer: Collects and posts transactions in batches to the Ethereum network.
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
Token Transfers: Users can send tokens to other addresses within the rollup.
Transaction Status: Users can monitor the status of transactions, whether in the mempool or already posted to Ethereum.
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
User Initiates Transfer: Alice decides to send 100 tokens to Bob. She enters Bob’s address and specifies a gas fee.
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.
Batch Poster Submits: The sequencer posts Alice’s transaction, along with others in the batch, to the
Inputs
contract on Ethereum.Backend Fetches Batch: The backend detects the new
BatchAppended
event and retrieves Alice’s transaction data.State Transition Function Updates: The STF processes Alice’s transaction, updating her balance and Bob’s balance in the rollup.
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.
Subscribe to my newsletter
Read articles from 0xKaushik directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
