🎉 Gate Square Growth Points Summer Lucky Draw Round 1️⃣ 2️⃣ Is Live!
🎁 Prize pool over $10,000! Win Huawei Mate Tri-fold Phone, F1 Red Bull Racing Car Model, exclusive Gate merch, popular tokens & more!
Try your luck now 👉 https://www.gate.com/activities/pointprize?now_period=12
How to earn Growth Points fast?
1️⃣ Go to [Square], tap the icon next to your avatar to enter [Community Center]
2️⃣ Complete daily tasks like posting, commenting, liking, and chatting to earn points
100% chance to win — prizes guaranteed! Come and draw now!
Event ends: August 9, 16:00 UTC
More details: https://www
Build your own Rollup – a list of BYOR projects
Compilation: Denlink translation plan
Have you ever wanted to learn more about how Rollup works? The theory is good, but hands-on experience is always preferable. Unfortunately, existing projects don't always make it easy to see what's going on. That's why we created BYOR (Build Your Own Rollup). It is a sovereign rollup with minimal functionality, with a focus on making the code easy to read and understand.
Our motivation for this project is for people, both outsiders and insiders, to better understand what the rollups around us are actually doing. You can play around on Holesky's deployed BYOR or read the source code on GitHub.
What is BYOR?
The BYOR project is a simplified version of sovereign rollup. In contrast to optimistic and zero-knowledge proofs of rollups, sovereign rollups do not validate state roots on Ethereum and rely only on data availability and consensus on Ethereum. This prevents trust-minimization bridges between L1 and BYOR, but greatly simplifies the code and is ideal for educational purposes.
The codebase consists of three programs: smart contracts, nodes, and wallets. When deployed together, they allow end users to interact with the network. Interestingly, the state of the network is determined entirely by on-chain data, which means that multiple nodes can actually run. Each node can also publish data independently as a sequencer.
Here is the full list of features implemented in BYOR:
Use the wallet
In a wallet app, it acts as the front end of the network, where users can submit transactions and check the status of their accounts or the status of transactions. On the landing page, you'll see an overview that provides some statistics about the current status of Rollup, followed by the status of your account. Most likely, there is only one button to connect to the wallet of your choice and there is news about the token faucet. Below, there's a search bar where you can paste someone's address or transaction hash to explore the current state of L2. Finally, there are two lists of transactions: the first is the list of transactions in the L2 mempool, and the second is the list of transactions published to L1.
To get started, use the WalletConnect button to connect your wallet. Once connected, you may receive a notification that your wallet is connected to the wrong network. If your application supports network switching, click the Switch Network button to switch to the Holesky test network. Otherwise, switch manually.
Now you can send tokens to someone by providing the recipient's address, the number of tokens to send, and the required fees. Once sent, the wallet app will prompt you to sign the message. If successfully signed, the message is sent to the L2 node's memory pool, waiting to be published to L1. The time it takes for a transaction to be bundled into a batch release may vary. Every 10 seconds, the L2 node checks for content to be published. Transactions with higher fees are sent first, so if you specify lower fees and have a lot of transaction traffic, you may experience long wait times.
How it works
We built each component using the following techniques:
Code drill down
BYOR code is designed to be easily understood by looking at the code base. Feel free to explore our codebase! First read README.md, to understand the project structure, please read the ARCHITECTURE.md file.
Here are some interesting highlights from the code:
Smart contracts
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);
}
}
This is the only smart contract needed. Its name derives from the fact that inputs are stored in state transition functions. The sole purpose of this contract is to conveniently store all transactions. The serialized batch is published to this smart contract as calldata, and it emits a BatchAppended event with the address of the batch publisher. While we can design the system so that it publishes transactions directly to EOA instead of contracts, data can be easily fetched via JSON-RPC by emitting events. The only requirement for this smart contract is that it should not be called from another smart contract, but directly from EOA.
Database schema
CREATE TABLE accounts (
address text PRIMARY KEY NOT NULL,
balance integer DEFAULT 0 NOT NULL,
nonce integer DEFAULT 0 NOT NULL
);
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,
feeReceipent text NOT NULL,
l1SubmittedDate integer NOT NULL,
hash text NOT NULL
PRIMARY KEY(from, nonce)
);
-- This table has a single row
CREATE TABLE fetcherStates (
chainId integer PRIMARY KEY NOT NULL,
lastFetchedBlock integer DEFAULT 0 NOT NULL
);
This is the entire database schema used to store information about Rollup. You might be wondering why we need a database when all the necessary data is stored on L1. While this is true, storing data locally can save time and resources by avoiding duplicate acquisitions. Treat all data stored in this schema as memos of status, transaction hashes, and other calculated information.
The fetcherStates table is used to keep track of the last block we fetched when searching for the BatchAppended event. This is useful when the node shuts down and restarts; It knows where to resume the search.
State transition functions
const DEFAULT_ACCOUNT = { balance: 0, nonce: 0 }
function uteTransaction(state, tx, feeRecipient) {
const fromAccount = getAccount(state, tx.from, DEFAULT_ACCOUNT)
const toAccount = getAccount(state, tx.to, DEFAULT_ACCOUNT)
// Step 1 Update nonce
fromAccount.nonce = tx.nonce
// Step 2 Transfer value
fromAccount.balance -= tx.value
toAccount.balance += tx.value
// Step 3 Pay fee
fromAccount.balance -= tx.fee
feeRecipientAccount.balance += tx.fee
}
The functions shown above are at the heart of the state transition mechanism in BYOR. It assumes that the transaction can be executed safely, with the correct nonce and sufficient balance to make the defined payout. Because of this assumption, there are no error handling or validation steps inside this function. Instead, these steps are performed before the function is called. Each account state is stored in a map. If an account does not already exist in this mapping, it will be set to the default value visible at the top of the code listing. Of the three accounts used, the nonce is updated and the balance is allocated.
Transaction Signing
L1 event get
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)
}
To get the new event, we retrieve all BatchAppended events from the last fetched block from the Inputs contract. The maximum number of events we retrieve is the most recent chunk or the last fetched chunk plus the batch size limit. After retrieving all events, we extract the calldata, timestamp, and publisher address from each transaction. Update the last block we fetch to the last block we are fetching. The extracted calldata, timestamp, and publisher are then packaged together and returned from the function for further processing.
Memory pools and their cost sorting
function popNHighestFee(txPool, n) {
txPool.sort((a, b) => b.fee - a.fee))
return txPool.splice(0, n)
}
A mempool is an object that manages an array of signed transactions. The most interesting aspect is how it determines the order in which transactions are posted to L1. As shown in the code above, transactions are sorted according to their fees. This allows the median fee price in the system to fluctuate based on on-chain activity.
Even if you specify high fees, transactions still need to produce a valid state if they need to be appended to the current state. Therefore, you can't submit invalid transactions just because of high fees.
Does BYOR really scale Ethereum?
Optimism and ZK rollup have built systems to prove that published state roots are consistent with state transition functions and the data they commit, but sovereign rollups do not. Therefore, the inability of this type of rollup to scale Ethereum may seem counterintuitive at first. However, this becomes reasonable when we realize that other types of rollups can use only L1 to prove that the published state root is correct. To distinguish whether the data of the sovereign rollup is correct or not, we need to run an L1 node along with additional software to formalize the L2 node to perform state transition functions, thereby increasing the computational load.
Future outlook
Building this project was a great learning experience for us, and we hope you'll find our efforts valuable as well. We hope to return to BYOR in the future and add a fraud proof system to it. This will make it a truly optimistic rollup and once again a lesson in the inner workings of the systems we use every day.