Skip to content

Creating Your Own L2 Rollup Testnet

This tutorial is designed for developers who want to create l2 rollup testnet chain. You’ll walk through the full deployment process and teach you all of the components that make up the l2 rollup, and you’ll end up with your own l2 rollup testnet.

You can use this testnet to experiment and perform tests, or you can choose to modify the chain to adapt it to your own needs.

Introduction

The opBNB stack is the Layer 2 scaling framework for the BNB Smart Chain powered by bedrock version of Optimism OP Stack. It works by offloading transaction processing and resource usage from the BNB Smart Chain, while still posting data to the underlying mainnet. Users can build their own L2 network with the framework. And then they can interact with the network by depositing funds from BSC and using applications and contracts on the network. Sequencers then aggregate transactions, compute state transitions and submit them to the rollup contract on BSC. Provers generate cryptographic proofs that prove the validity of these state transitions, and Verifiers check the proofs to verify the L2 network’s state is correct. At its core, it allows users to deposit and withdraw funds, use smart contracts, and view network data with high throughput and low fees.

image-20250411135104222

What You’re Going to Deploy

Deployment Architecture

image-20250411135320376

Sequencer

Proposer

Batcher

Bridge node

RPC node

When deploying a l2 rollup chain, you’ll be setting up four different components. It’s useful to understand what each of these components does before you start deploying your chain.

Smart Contracts

The l2 rollup gives you the ability to deploy your own l2 rollup that use a Layer 1 blockchain to host and order transaction data. The l2 rollup uses several smart contracts on the L1 blockchain to manage aspects of the Rollup. You’ll be using the L1 smart contracts found in the contracts-bedrock package within the opbnb.

Sequencer Node

The l2 rollup uses Sequencer node to gather proposed transactions from users and publish them to the L1 blockchain.

Consensus Client

The l2 rollup has a consensus client. The consensus client is responsible for determining the list and ordering of blocks and transactions that are part of your blockchain. In this tutorial you’ll be using the op-node found within the opbnb.

Execution Client

The l2 rollup has an execution client. The execution client is responsible for executing transactions and storing/updating the state of the blockchain. In this tutorial you’ll be using the op-geth found within the op-geth repository.

Each of the Sequencer, Bridge, and RPC nodes runs both an op-node and an op-geth process—the sequencer start op-node process with a miner flag and the others run with fullnode flag . The key difference between an RPC node and a bridge node is that an RPC node exposes a public endpoint for external users to access, while a bridge node is not externally reachable. The bridge node functions like a cache for sequencer node : it forwards incoming transactions to the sequencer, helping prevent the sequencer’s transaction pool from being overloaded.

Batcher

The Batcher is a service that publishes transactions from the Sequencer to the L1 blockchain. The Batcher runs continuously alongside the Sequencer and publishes transactions in batches (hence the name) on a regular basis. You’ll be using the op-batcher of the Batcher component found within the opbnb.

Proposer

The Proposer is a service responsible for publishing transactions results (in the form of L2 state roots) to the L1 blockchain. This allows smart contracts on L1 to read the state of the L2, which is necessary for cross-chain communication and reconciliation between state changes. You’ll be using the op-proposer of the Proposer component found within the opbnb.

Software Dependencies

Dependency Version Version Check Command
git ^2 git –version
go ^1.21 go version
node ^20 node –version
pnpm ^8 pnpm –version
foundry ^0.2.0 forge –version
make ^3 make –version
jq ^1.6 jq –version
direnv ^2 direnv –version
gcc ^13 gcc –version
libc-dev ^2.35 ldd –version

Hardware requirements

CPU: 4 × 2 GHz (or faster) 64‑bit processor cores

Memory: 16 GB DDR4 RAM

For op-geth (execution layer):

  • High‑performance SSD with sustained IOPS ≥ 20,000, or an enterprise-grade NVMe SSD (to ensure low latency under heavy state‑healing workloads) gist.githubdocs.optimism
  • ≥ 3 TB capacity

For op-node (consensus & aggregation):

  • Standard SSD (SATA or consumer NVMe) is sufficient, ≥ 500 GB

Notes on Specific Dependencies

node

We recommend using the latest LTS version of Node.js (currently v20). nvm is a useful tool that can help you manage multiple versions of Node.js on your machine. You may experience unexpected errors on older versions of Node.js.

foundry

It’s recommended to use the scripts in the opbnb’s package.json for managing foundry to ensure you’re always working with the correct version. This approach simplifies the installation, update, and version checking process. Make sure to clone the opbnb locally before proceeding.

direnv

Parts of this tutorial use direnv as a way of loading environment variables from .envrc files into your shell. This means you won’t have to manually export environment variables every time you want to use them. direnv only ever has access to files that you explicitly allow it to see.

After installing direnv, you will need to make sure that direnv is hooked into your shell. Make sure you’ve followed the guide on the direnv website, then close your terminal and reopen it so that the changes take effect (or source your config file if you know how to do that).

Get Access to a BSC testnet Node

You’ll be deploying a BSC l2 rollup that uses a Layer 1 blockchain to host and order transaction data.

This guide uses the BSC testnet as an L1 chain. You can also use other EVM-compatible blockchains, but you may run into unexpected errors. If you want to use an alternative network, make sure to carefully review each command and replace any BSC testnet values with the values for your network.

Since you’re deploying your l2 rollup on BSC testnet, you’ll need to have access to a BSC testnet node.

Build the Source Code

You’re going to be creating your own l2 rollup directly from source code instead of using a container system like Docker. Although this adds a few extra steps, it means you’ll have an easier time modifying the behavior of the l2 rollup if you’d like to do so. If you want a summary of the various components you’ll be using, take another look at the What You’re Going to Deploy section above.

Build the opbnb

Clone the opbnb

cd ~
git clone https://github.com/bnb-chain/opbnb.git

Enter the opbnb

cd opbnb

Check out the correct branch

You can choose the appropriate version according to your needs. The develop branch has the latest features.

git checkout -b v0.5.3 v0.5.3-hotfix

Install dependencies

pnpm install

Build the various packages inside of the opbnb

make op-node op-batcher op-proposer

Build op-geth

Clone op-geth

cd ~
git clone https://github.com/bnb-chain/op-geth.git

Check out the correct branch

You can choose the appropriate version according to your needs. The develop branch has the latest features.

git checkout -b v0.5.7 

Enter op-geth

cd op-geth

Build op-geth

make geth

Fill Out Environment Variables

You’ll need to fill out a few environment variables before you can start deploying your chain.

Enter the opbnb

cd ~/opbnb

Duplicate the sample environment variable file

cp .envrc.example .envrc

Fill out the environment variable file

Open up the environment variable file and fill out the following variables:

Variable Name Description
L1_RPC_URL URL for your L1 node
L1_RPC_KIND Kind of L1 RPC you’re connecting to, used to inform optimal transactions receipts fetching(Optional).
L1_CHAIN_ID The Chain id of L1 chain ( bsc testnet is 97)
L1_BLOCK_TIME The block interval value of L1 CHAIN
L2_CHAIN_ID The Chain id of Layer 2 chain
L2_BLOCK_TIME The block interval value of L2 CHAIN

Generate Addresses

You’ll need four addresses and their private keys when setting up the chain:

  • The Admin address has the ability to upgrade contracts.
  • The Batcher address publishes Sequencer transaction data to L1.
  • The Proposer address publishes L2 transaction results (state roots) to L1.
  • The Sequencer address signs blocks on the p2p network.

Enter the opbnb

cd ~/opbnb

Generate new addresses

./packages/contracts-bedrock/scripts/getting-started/wallets.sh

Check the output

Make sure that you see output that looks something like the following:

Copy the following into your .envrc file:

# Admin account
export GS_ADMIN_ADDRESS=0xC18e23D98F121c48a56E19302D1B7FB9b82A0F2E
export GS_ADMIN_PRIVATE_KEY=0x5d5e555305c69711eb31dd24dd1530b137489c54ddc83afd421f581c3fbf6c67

# Batcher account
export GS_BATCHER_ADDRESS=0x7527Cc2860B71E654a98235c8CF5D8Ca792040FE
export GS_BATCHER_PRIVATE_KEY=0xf6c2a7cf909a41fc227b03cf6474c66eaff5e05b36a89394f5a5129598fa8d13

# Proposer account
export GS_PROPOSER_ADDRESS=0xeE7579518904123AE4b3BaB729A4B9c9c08D5658
export GS_PROPOSER_PRIVATE_KEY=0x95176e9e67c1b97b4b7aaedf66e0b34b446f8683d545b44214240f6f1cfa6891

# Sequencer account
export GS_SEQUENCER_ADDRESS=0xeCF961D156a2ce02E98Ad26E79De62BcFd403cfd
export GS_SEQUENCER_PRIVATE_KEY=0xbb019ddc5f081b2be0c1c9406f89887ba90f4c8efbe5c4073ee067e56d3107ea

Save the addresses

Copy the output from the previous step and paste it into your .envrc file as directed.

Fund the addresses

You will need to send BNB to the Admin, Proposer, and Batcher addresses. The exact amount of BNB required depends on the L1 network being used. You do not need to send any BNB to the Sequencer address as it does not send transactions.

It’s recommended to fund the addresses with the following amounts when using BSC testnet:

  • Admin — 1 BNB

  • Proposer — 0.5 BNB

  • Batcher — 2 BNB

Load Environment variables

Now that you’ve filled out the environment variable file, you need to load those variables into your terminal.

Enter the opbnb

cd ~/opbnb

Load the variables with direnv

You’re about to use direnv to load environment variables from the .envrc file into your terminal. Make sure that you’ve installed direnv and that you’ve properly hooked direnv into your shell. Next you’ll need to allow direnv to read this file and load the variables into your terminal using the following command.

direnv allow

direnv will unload itself whenever your .envrc file changes. You must rerun the following command every time you change the .envrc file.

Confirm that the variables were loaded

After running direnv allow you should see output that looks something like the following (the exact output will vary depending on the variables you’ve set, don’t worry if it doesn’t look exactly like this):

direnv: loading ~/optimism/.envrc                                                            
direnv: export +DEPLOYMENT_CONTEXT +ETHERSCAN_API_KEY +GS_ADMIN_ADDRESS +GS_ADMIN_PRIVATE_KEY +GS_BATCHER_ADDRESS +GS_BATCHER_PRIVATE_KEY +GS_PROPOSER_ADDRESS +GS_PROPOSER_PRIVATE_KEY +GS_SEQUENCER_ADDRESS +GS_SEQUENCER_PRIVATE_KEY +IMPL_SALT +L1_RPC_KIND +L1_RPC_URL +PRIVATE_KEY +TENDERLY_PROJECT +TENDERLY_USERNAME

If you don’t see this output, you likely haven’t properly configured direnv. Make sure you’ve configured direnv properly and run direnv allow again so that you see the desired output.

If you don’t want to use direnv, you can also set variables manually.

Configure your network

Once you’ve built both repositories, you’ll need to head back to the opbnb to set up the configuration file for your chain. Currently, chain configuration lives inside of the contracts-bedrock package in the form of a JSON file.

Enter the opbnb

cd ~/opbnb

Move into the contracts-bedrock package

cd packages/contracts-bedrock

Generate the configuration file

Run the following script to generate the getting-started.json configuration file inside of the deploy-config directory.

./scripts/getting-started/config.sh

The script-based approach shown above is the one officially recommended by OP, and the generated configuration is the official version. However, we strongly recommend using the template method instead. A template is provided at the end of this document. You can use the information from getting-started.json together with the template to generate an OPBNB-style configuration

In the template, in addition to replacing the Chain Info and all address fields generated earlier, you must also specify the following three parameters:

1)l1BlockTag

2)l2OutputOracleStartingTimestamp

3)l2GenesisBlockTimestamp

The example script below demonstrates how to populate the configuration using our template.

def prepareConfig(keys,paths,env):
    log.info('prepare qanet config starting')
    if os.path.exists(paths.qanet_config_path):
        log.info('qanet config already exists,skip')
        deploy_config = read_json(paths.qanet_config_path)
        env['L2_CHAIN_ID']=deploy_config['l2ChainID']
        env['BATCH_INBOX_ADDRESS']=deploy_config['batchInboxAddress']
        return

    deploy_config = read_json(paths.devnet_config_template_path)
    l1_rpc = env['L1_RPC_URL']
    l1BlockTag = blockTagGet(l1_rpc)["result"]
    log.info(l1BlockTag)
    l1BlockTimestamp = blockTimestampGet(l1_rpc,l1BlockTag)["result"]["timestamp"]
    log.info(l1BlockTimestamp)
    l1ChainId = chainId(l1_rpc)["result"]
    log.info(l1ChainId)
    l2ChainId = decide_l2_chain_id(paths,env)
    log.info(f'decide l2 chain id is:{l2ChainId}')
    env['L2_CHAIN_ID']=l2ChainId
    env['BATCH_INBOX_ADDRESS']='0xff0000000000000000000000000000000000'+str(l2ChainId)

    deploy_config['l1GenesisBlockTimestamp'] = l1BlockTimestamp
    deploy_config['l1StartingBlockTag'] = l1BlockTag
    deploy_config['l1ChainID'] = int(l1ChainId,16)
    deploy_config['l2ChainID'] = l2ChainId
    deploy_config['l2BlockTime'] = 1
    deploy_config['maxSequencerDrift'] = 600
    deploy_config['sequencerWindowSize'] = 57600
    deploy_config['channelTimeout'] = 1200
    deploy_config['p2pSequencerAddress'] = keys['op_p2p_address']
    deploy_config['batchInboxAddress'] = env['BATCH_INBOX_ADDRESS']
    deploy_config['batchSenderAddress'] = keys['op_batcher_address']
    deploy_config['l2OutputOracleSubmissionInterval'] = 240
    deploy_config['l2OutputOracleStartingTimestamp'] = int(l1BlockTimestamp,16)
    deploy_config['l1UseClique'] = False
    deploy_config['l2OutputOracleProposer'] = keys['op_proposer_address']
    deploy_config['l2OutputOracleChallenger'] = keys['op_admin_address']
    deploy_config['l2GenesisBlockGasLimit'] = '0x5f5e100'
    deploy_config['baseFeeVaultRecipient'] = keys['op_admin_address']
    deploy_config['l1FeeVaultRecipient'] = keys['op_admin_address']
    deploy_config['sequencerFeeVaultRecipient'] = keys['op_admin_address']
    deploy_config['proxyAdminOwner'] = keys['op_admin_address']
    deploy_config['finalSystemOwner'] = keys['op_admin_address']
    deploy_config['finalizationPeriodSeconds'] = 3
    deploy_config['l2GenesisBlockBaseFeePerGas'] = '0x5F5E100'
    deploy_config['enableGovernance'] = False
    deploy_config['governanceTokenSymbol'] = 'OPBNB'
    deploy_config['governanceTokenName'] = 'OPBNB'
    deploy_config['governanceTokenOwner'] = keys['op_admin_address']
    deploy_config['eip1559Denominator'] = 8
    deploy_config['eip1559DenominatorCanyon'] = 8
    deploy_config['eip1559Elasticity'] = 2
    deploy_config['fermat'] = 0
    deploy_config['l2GenesisDeltaTimeOffset'] = "0x0"
    deploy_config['L2GenesisEcotoneTimeOffset'] = "0x0"
    deploy_config['l2GenesisFjordTimeOffset'] = "0x0"
    deploy_config['snowTimeOffset'] = "0x0"
    deploy_config['haberTimeOffset'] = "0x0"
    deploy_config['wrightTimeOffset'] = "0x0"
    write_json(paths.qanet_config_path,deploy_config)
    shutil.copy(paths.qanet_config_path, paths.qanet_config_output_path)
    log.info(f'prepared qanet config,path:{paths.qanet_config_path},content:{deploy_config}')



 def blockTagGet(url):
        return jsonRpcGet1(url,'{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":74}')


def blockTimestampGet(url,block_tag):
    return jsonRpcGet(url,f'{{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["{block_tag}", false],"id":74}}')

def write_json(path, data):
    with open(path, 'w+') as f:
        json.dump(data, f, indent='  ')
def read_json(path):
    with open(path, 'r') as f:
        return json.load(f)

You can also fetch the three values—l1BlockTag, l2OutputOracleStartingTimestamp, and l2GenesisBlockTimestamp—via curl (here shown against BSC Testnet):

// get tag info
curl -s -X POST https://data-seed-prebsc-2-s2.bnbchain.org:8545  -H "Content-Type: application/json"   -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":74}'


curl -s -X POST https://data-seed-prebsc-1-s1.bnbchain.org:8545   -H "Content-Type: application/json"   -d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x2e10b2c", false],"id":74}'

// In the returned result, the timestamp field is your l2GenesisBlockTimestamp. And converting that value to a Unix‐style (seconds) timestamp gives you the l2OutputOracleStartingTimestamp.

Review and change the configuration file (Optional)

If you’d like, you can review the configuration file that was just generated by opening up deploy-config/getting-started.json in your favorite text editor. You can change configuration values to fit your specific needs. Please refer the Chain Configuration. It’s recommended to keep this file as-is for now so you don’t run into any unexpected errors.

Deploy the Create2 Factory (Optional)

If you’re deploying opBNB to a network other than BSC testnet, you may need to deploy a Create2 factory contract to the L1 chain. This factory contract is used to deploy opBNB smart contracts in a deterministic fashion.

Check if the factory exists

The Create2 factory contract will be deployed at the address 0x4e59b44847b379578588920cA78FbF26c0B4956C. You can check if this contract has already been deployed to your L1 network with a block explorer or by running the following command:

cast codesize 0x4e59b44847b379578588920cA78FbF26c0B4956C --rpc-url $L1_RPC_URL

If the command returns 0 then the contract has not been deployed yet. If the command returns 69 then the contract has been deployed and you can safely skip this section.

Fund the factory deployer

You will need to send some BNB to the address that will be used to deploy the factory contract, 0x3fAB184622Dc19b6109349B94811493BF2a45362. This address can only be used to deploy the factory contract and will not be used for anything else. Send at least 1 BNB to this address on your L1 chain.

Deploy the factory

Using cast, deploy the factory contract to your L1 chain:

cast publish --rpc-url $L1_RPC_URL 0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222 

Wait for the transaction to be mined

Make sure that the transaction is included in a block on your L1 chain before continuing.

Verify that the factory was deployed

Run the code size check again to make sure that the factory was properly deployed:

cast codesize 0x4e59b44847b379578588920cA78FbF26c0B4956C --rpc-url $L1_RPC_URL

Deploy the L1 contracts

Once you’ve configured your network, it’s time to deploy the L1 contracts necessary for the functionality of the chain.

Deploy the L1 contracts

Add .env file in the current directory where you run the script or export follow content to .envrc file to make sure the following info can be loaded. The most important variable is DEPLOY_CONFIG_PATH, which should point to the configuration file you generated above. For example:

cat .env
```
DEPLOYMENT_OUTFILE="./deployments/L1/.deploy"    // The contract address information entered after deployment.
DEPLOY_CONFIG_PATH="./deploy-config/devnet.json"  // config file
DEPLOYMENT_CONTEXT="devnetL1"
IMPL_SALT="ethers phoenixtestnetnewimpl"
forge script scripts/Deploy.s.sol:Deploy --private-key $GS_ADMIN_PRIVATE_KEY --broadcast --rpc-url $L1_RPC_URL --slow

If you see a nondescript error that includes EvmError: Revert and Script failed then you likely need to change the IMPL_SALT environment variable. This variable determines the addresses of various smart contracts that are deployed via CREATE2. If the same IMPL_SALT is used to deploy the same contracts twice, the second deployment will fail. You can generate a new IMPL_SALT by running direnv allow anywhere in the opbnb.

L1 Contracts

Contracts Description comments
SystemConfig Responsible for saving the system configuration and providing the ability to change it online.These configurations will be used by L2 block derivation process including overhead, scalar, batcherHash, gasLimit, resourceConfig,etc.
L1StandardBridge Responsible for the transfer of ERC20 tokens between L1 and L2.
L1ERC721Bridge Responsible for the transfer of ERC721 tokens between L1 and L2.
L1CrossDomainMessenger Responsible for delivering messages and being more user friendly.
OptimismPortal Responsible for passing messages between L1 and L2, which is equivalent to the portal of the optimistic network. Validate output root to execute L2 to L1 message call.
L2OutputOracle Responsible for storing the output root of each block of the L2 network. Other contracts or users can verify the validity of L2 data according to the output root.

image-20250411151438331

Generate the L2 config files

Now that you’ve set up the L1 smart contracts you can automatically generate several configuration files that are used within the Consensus Client and the Execution Client.

You need to generate three important files:

  1. genesis.json includes the genesis state of the chain for the Execution Client (sequencer).
  2. rollup.json includes configuration information for the Consensus Client (bridage node, rpcnode ,p2p node).
  3. jwt.txt is a JSON Web Token that allows the Consensus Client and the Execution Client to communicate securely (the same mechanism is used in Ethereum clients).
cd ~/opbnb/op-node

Create genesis files

Now you’ll generate the genesis.json and rollup.json files within the op-node folder:

–deploy-config 和 –l1-deployments flag info needs to be replaced by th DEPLOY_CONFIG_PATH and DEPLOYMENT_OUTFILE of .env or .envrc

go run cmd/main.go genesis l2 \
  --deploy-config ../packages/contracts-bedrock/deploy-config/getting-started.json \
  --l1-deployments ../packages/contracts-bedrock/deployments/getting-started/.deploy \
  --outfile.l2 genesis.json \
  --outfile.rollup rollup.json \
  --l1-rpc $L1_RPC_URL

Following is a example of rollup.json

{
  "genesis": {
    "l1": {
      "hash": "0xf48f87b325e9e8e73bf6964346588b5f56156128278e18d74aa7ee0ceba6a714",
      "number": 5792
    },
    "l2": {
      "hash": "0x0369f047f1e2d5975a4d088825b55d97bf1a0fc5c3df6e3aebd8f6e2f1e086f2",
      "number": 0
    },
    "l2_time": 1743059438,
    "system_config": {
      "batcherAddr": "0x29cb7f616ab5c10bc59a7ed59e95ae6a2f8f0949",
      "overhead": "0x0000000000000000000000000000000000000000000000000000000000000834",
      "scalar": "0x00000000000000000000000000000000000000000000000000000000000f4240",
      "gasLimit": 100000000
    }
  },
  "block_time": 1,
  "max_sequencer_drift": 600,
  "seq_window_size": 57600,
  "channel_timeout": 1200,
  "l1_chain_id": 714,
  "l2_chain_id": 3717,
  "regolith_time": 0,
  "canyon_time": 0,
  "delta_time": 0,
  "ecotone_time": 0,
  "fjord_time": 0,
  "volta_time": 1743094185,
  "fermat": 0,
  "snow_time": 0,
  "batch_inbox_address": "0xff00000000000000000000000000000000003717",
  "deposit_contract_address": "0x0da507e81c31bcfb7088524c31b9d99bd020a315",
  "l1_system_config_address": "0x89a9dcfc964e4b9b818be2552735ca17ce13e574",
  "protocol_versions_address": "0x0000000000000000000000000000000000000000",
  "da_challenge_contract_address": "0x0000000000000000000000000000000000000000"
}

The latest op code supports the volta_time configuration to support Volta fork which makes the block time as 500ms. If you need to set the Layer 2 block time to less than 1 second—i.e., support millisecond-level block intervals—there are two ways to achieve this:

  1. Manually add volta_time to the configuration file and set it to 0. This will enable 500ms block time immediately.
  2. If you plan to perform a hard fork upgrade at a future time, set volta_time to a future timestamp. We will provide a separate document to explain the hard fork upgrade process.

Create an authentication key

Next you’ll create a JSON Web Token that will be used to authenticate the Consensus Client and the Execution Client. This token is used to ensure that only the Consensus Client and the Execution Client can communicate with each other. You can generate a JWT with the following command:

openssl rand -hex 32 > jwt.txt

Copy genesis files into the op-geth directory

Finally, you’ll need to copy the genesis.json file and jwt.txt file into op-geth so you can use it to initialize and run op-geth:

cp genesis.json ~/op-geth
cp jwt.txt ~/op-geth

Initialize op-geth

You’re almost ready to run your chain! Now you just need to run a few commands to initialize op-geth. You’re going to be running a Sequencer node, so you’ll need to import the Sequencer private key that you generated earlier. This private key is what your Sequencer will use to sign new blocks.

cd ~/op-geth

Create a data directory folder

mkdir datadir

Initialize op-geth

build/bin/geth init --datadir=datadir  --state.scheme path --db.engine genesis.json

Start op-geth

Now you’ll start op-geth, your Execution Client. Note that you won’t start seeing any transactions until you start the Consensus Client in the next step.

Open up a new terminal

You’ll need a terminal window to run op-geth in.

cd ~/op-geth

Run op-geth

You’re using --gcmode=archive to run op-geth here because this node will act as your Sequencer. It’s useful to run the Sequencer in archive mode because the op-proposer requires access to the full state. Feel free to run other (non-Sequencer) nodes in full mode if you’d like to save disk space. It’s important that you’ve already initialized the geth node at this point as per the previous section. Failure to do this will cause startup issues between op-geth and op-node.

 ./build/bin/geth \
  --datadir ./datadir \
  --http \
  --http.corsdomain="*" \
  --http.vhosts="*" \
  --http.addr=0.0.0.0 \
  --http.api=web3,debug,eth,txpool,net,engine \
  --ws \
  --ws.addr=0.0.0.0 \
  --ws.port=8546 \
  --ws.origins="*" \
  --ws.api=debug,eth,txpool,net,engine \
  --syncmode=full \
  --gcmode=archive \
  --nodiscover \
  --maxpeers=0 \
  --networkid=5611 (replace with your rollup chaind id ) \
  --authrpc.vhosts="*" \
  --authrpc.addr=0.0.0.0 \
  --authrpc.port=8551 \
  --txpool.globalslots=40000 \
  --txpool.globalqueue=10000 \
  --txpool.accountqueue=16 \
  --txpool.accountslots=16 \
  --txpool.pricelimit=1 \
  --txpool.nolocals=true \
  --txpool.reannouncetime=3m \
  --txpool.reannounceremotes=true \
  --cache=2000 \
  --authrpc.jwtsecret=./jwt.txt \
  --rollup.disabletxpoolgossip=true

Start op-node

Once you’ve got op-geth running you’ll need to run op-node. Like Ethereum, the opBNB has a Consensus Client (op-node) and an Execution Client (op-geth). The Consensus Client “drives” the Execution Client over the Engine API. Each of the Sequencer, Bridge, P2P, and RPC nodes runs both an op-node and an op-geth process—the only distinction is the set of startup parameters supplied to the op-node.

Open up a new terminal

You’ll need a terminal window to run op-node in.

cd ~/opbnb/op-node

Run op-node as miner (sequencer)

./bin/op-node \
  --l2=http://localhost:8551 \
  --l2.jwt-secret=./jwt.txt \
  --sequencer.enabled \
  --sequencer.l1-confs=15 \
  --verifier.l1-confs=15 \
  --l1.http-poll-interval=3s \
  --l1.epoch-poll-interval=3s \
  --l1.rpc-max-batch-size=20 \
  --rollup.config=./rollup.json \
  --rpc.addr=0.0.0.0 \
  --rpc.port=8547 \
  --p2p.disable \
  --rpc.enable-admin \
  --p2p.sequencer.key=$GS_SEQUENCER_PRIVATE_KEY \
  --l1=$L1_RPC_URL \
  --l1.rpckind=$L1_RPC_KIND

Once you run this command, you should start seeing the op-node begin to sync L2 blocks from the L1 chain. Once the op-node has caught up to the tip of the L1 chain, it’ll begin to send blocks to op-geth for execution. At that point, you’ll start to see blocks being created inside of op-geth. By default, your op-node will try to use a peer-to-peer to speed up the synchronization process. If you’re using a chain ID that is also being used by others, like the default chain ID for this tutorial (42069), your op-node will receive blocks signed by other sequencers. These requests will fail and waste time and network resources. To avoid this, this tutorial starts with peer-to-peer synchronization disabled (--p2p.disable).

Once you have multiple nodes, you may want to enable peer-to-peer synchronization. You can add the following options to the op-node command to enable peer-to-peer synchronization with specific nodes:

--p2p.static=<nodes> \
--p2p.listen.ip=0.0.0.0 \
--p2p.listen.tcp=9003 \
--p2p.listen.udp=9003 \

You can alternatively also remove the --p2p.static option, but you may see failed requests from other chains using the same chain ID.

Run as Bridage node or RPC node

./bin/op-node 
     --l2=http://localhost:8551 
   --l1.trustrpc
   --l2.jwt-secret=./jwt.txt 
   --sequencer.l1-confs=15
   --verifier.l1-confs=15
   --l1.http-poll-interval=3s
   --l1.epoch-poll-interval=3s
   --l1.rpc-max-batch-size=20
   --rollup.config=/server/rollup.json
   --rpc.addr=0.0.0.0 \
   --rpc.port=8547 \
   --p2p.disable \
   --rpc.enable-admin \
   --l1=$L1_RPC_URL \
   --l1.rpckind=$L1_RPC_KIND
    --l1.max-concurrency=20   --log.level=debug
   --syncmode=execution-layer
   --l1.max-concurrency=20

To run a Bridge node , you can refer to this command, you need to adjust the flags by your environment..

Start op-batcher

The op-batcher takes transactions from the Sequencer and publishes those transactions to L1. Once these Sequencer transactions are included in a finalized L1 block, they’re officially part of the canonical chain. The op-batcher is critical!

It’s best to give the Batcher address at least 2 BNB to ensure that it can continue operating without running out of BNB for gas. Keep an eye on the balance of the Batcher address because it can expend BNB quickly if there are a lot of transactions to publish.

Open up a new terminal

You’ll need a terminal window to run op-batcher in.

cd ~/opbnb/op-batcher

Run op-batcher

./bin/op-batcher \
  --l2-eth-rpc=http://localhost:8545 \
  --rollup-rpc=http://localhost:8547 \
  --poll-interval=5s \
  --sub-safety-margin=30 \
  --num-confirmations=4 \
  --safe-abort-nonce-too-low-count=3 \
  --resubmission-timeout=30s \
  --rpc.addr=0.0.0.0 \
  --rpc.port=8548 \
  --rpc.enable-admin \
  --max-channel-duration=20 \
  --l1-eth-rpc=$L1_RPC_URL \
  --private-key=$GS_BATCHER_PRIVATE_KEY

The --max-channel-duration=n setting tells the batcher to write all the data to L1 every n L1 blocks. When it is low, transactions are written to L1 frequently and other nodes can synchronize from L1 quickly. When it is high, transactions are written to L1 less frequently and the batcher spends less BNB. If you want to reduce costs, either set this value to 0 to disable it or increase it to a higher value.

Start op-proposer

Open up a new terminal

You’ll need a terminal window to run op-proposer in.

cd ~/opbnb/op-proposer

Run op-proposer

./bin/op-proposer \
  --poll-interval=1s \
  --rpc.port=8560 \
  --rollup-rpc=http://localhost:8547 \
  --l2oo-address=$(cat ../packages/contracts-bedrock/deployments/getting-started/.deploy | jq -r .L2OutputOracleProxy) \
  --private-key=$GS_PROPOSER_PRIVATE_KEY \
  --l1-eth-rpc=$L1_RPC_URL

Connect Your Wallet to Your Chain

You now have a fully functioning l2 rollup chain with a Sequencer node running on http://localhost:8545. You can connect your wallet to this chain the same way you’d connect your wallet to any other EVM chain.

Get BNB On Your Chain

Once you’ve connected your wallet, you’ll probably notice that you don’t have any BNB to pay for gas on your chain. The easiest way to deposit BNB into your chain is to send BNB directly to the L1StandardBridge contract.

cd ~/opbnb/packages/contracts-bedrock

Get the address of the L1StandardBridgeProxy contract

cat deployments/getting-started/.deploy | jq -r .L1StandardBridgeProxy

Send some BNB to the L1StandardBridgeProxy contract

Grab the L1 bridge proxy contract address and, using the wallet that you want to have BNB on your l2 rollup chain, send that address a small amount of BNB on L1. This will trigger a deposit that will mint BNB into your wallet on L2. It may take up to some minutes for that BNB to appear in your wallet on L2.

See Your opBNB in Action

You can interact with your l2 the same way you’d interact with any other EVM chain. Send some transactions, deploy some contracts, and see what happens!

Deploy config example

{
    "l1ChainID": 97,
    "l2ChainID": 4255,
    "l2BlockTime": 1,
    "maxSequencerDrift": 600,
    "sequencerWindowSize": 57600,
    "channelTimeout": 1200,
    "p2pSequencerAddress": "0x3224F068Fd97D2d4a91800D450EE19bfBCD6B6Ee",
    "batchInboxAddress": "0xff00000000000000000000000000000000000901",
    "batchSenderAddress": "0xB6e487a3cEcDe5e0E1793C98c2de1999a1319A2b",
    "cliqueSignerAddress": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    "l1UseClique": false,
    "l1StartingBlockTag": "0x2fa3b94",
    "l2OutputOracleSubmissionInterval": 240,
    "l2OutputOracleStartingBlockNumber": 0,
    "l2OutputOracleStartingTimestamp": 1744595989,
    "l2OutputOracleProposer": "0x254811af494550Ee5e0945C23EC5E4E17c9dF1bC",
    "l2OutputOracleChallenger": "0x5E8fe030B465b448a78bA5Ef3c674Da25fDf67f5",
    "l2GenesisBlockGasLimit": "0x5f5e100",
    "l1BlockTime": 3,
    "baseFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000",
    "l1FeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000",
    "sequencerFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000",
    "baseFeeVaultWithdrawalNetwork": 0,
    "l1FeeVaultWithdrawalNetwork": 0,
    "sequencerFeeVaultWithdrawalNetwork": 0,
    "proxyAdminOwner": "0x5E8fe030B465b448a78bA5Ef3c674Da25fDf67f5",
    "baseFeeVaultRecipient": "0x5E8fe030B465b448a78bA5Ef3c674Da25fDf67f5",
    "l1FeeVaultRecipient": "0x5E8fe030B465b448a78bA5Ef3c674Da25fDf67f5",
    "sequencerFeeVaultRecipient": "0x5E8fe030B465b448a78bA5Ef3c674Da25fDf67f5",
    "finalSystemOwner": "0x5E8fe030B465b448a78bA5Ef3c674Da25fDf67f5",
    "superchainConfigGuardian": "0x5E8fe030B465b448a78bA5Ef3c674Da25fDf67f5",
    "finalizationPeriodSeconds": 3,
    "fundDevAccounts": true,
    "l2GenesisBlockBaseFeePerGas": "0x5F5E100",
    "gasPriceOracleOverhead": 2100,
    "gasPriceOracleScalar": 1000000,
    "gasPriceOracleBaseFeeScalar": 1368,
    "gasPriceOracleBlobBaseFeeScalar": 810949,
    "enableGovernance": false,
    "governanceTokenSymbol": "OPBNB",
    "governanceTokenName": "OPBNB",
    "governanceTokenOwner": "0x5E8fe030B465b448a78bA5Ef3c674Da25fDf67f5",
    "eip1559Denominator": 8,
    "eip1559DenominatorCanyon": 8,
        "eip1559Elasticity": 2,
    "l1GenesisBlockTimestamp": "0x67f88995",
    "l2GenesisRegolithTimeOffset": "0x0",
    "l2GenesisDeltaTimeOffset": "0x0",
    "l2GenesisCanyonTimeOffset": "0x0",
    "systemConfigStartBlock": 0,
    "requiredProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "recommendedProtocolVersion": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "faultGameAbsolutePrestate": "0x03c7ae758795765c6664a5d39bf63841c71ff191e9189522bad8ebff5d4eca98",
    "faultGameMaxDepth": 50,
    "faultGameClockExtension": 0,
    "faultGameMaxClockDuration": 1200,
    "faultGameGenesisBlock": 0,
    "faultGameGenesisOutputRoot": "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
    "faultGameSplitDepth": 14,
    "faultGameWithdrawalDelay": 604800,
    "preimageOracleMinProposalSize": 10000,
    "preimageOracleChallengePeriod": 120,
    "proofMaturityDelaySeconds": 12,
    "disputeGameFinalityDelaySeconds": 6,
    "respectedGameType": 254,
    "useFaultProofs": false,
    "usePlasma": false,
    "daCommitmentType": "KeccakCommitment",
    "daChallengeWindow": 160,
    "daResolveWindow": 160,
    "daBondSize": 1000000,
    "daResolverRefundPercentage": 0,
    "fermat": 0,
    "L2GenesisEcotoneTimeOffset": "0x0",
    "l2GenesisFjordTimeOffset": "0x0",
    "snowTimeOffset": "0x0",
    "haberTimeOffset": "0x0",
    "wrightTimeOffset": "0x0"
}

Configuration explain

Chain Configuration

Item Type Description 中文补充描述 Default Value
Chain ID number Chain id 链id
Gas limit number The maximum gas usage of the block 区块最大gas使用量 100M
Public RPC url Rpc domain name 公开的Rpc 域名
Internal RPC URL URL 内部使用的RPC域名
l2BlockTime number L2 block time l2 区块时间 0.5 second
Withdraw time Time period Withdraw challenge period Withdraw的挑战期 7 days
Batcher Commit interval Time period Batcher interval for submitting data Batcher 提交数据的最大间隔 1 mins
ProposerCommit interval Time period Proposer interval for submitting data Proposer 提交数据的间隔 1 hour

Gas fee Configuration

Item Type Description 中文补充描述 Default Value
l1FeeScalar number Gas fee to be Submitted to layer 1, can be configured by Layer 2 operators, which will decide whether layer 2 is profitable 提交给 Layer 1 的 Gas 费用,可由 Layer 2 运营商配置,这将决定 Layer 2 是否盈利 1000000

Governance Configuration

Item Type Description 中文补充描述 Default Value
Support Governance Token bool Whether need to support Governance Token 是否需要支持Governance Token false
Governance Token Symbol String Symbol for the token deployed by default to each OP Stack chain. 默认部署到每个 OP BNB 链的代币符号。
Governance Token Name Governance Token Owner L2 Address Address that will own the token contract deployed by default to every OP Stack based chain.Multisig address supported 管理Governance Token

Custom Gas Token Configuration

Item Type Description 中文补充描述 Default Value
Token Name String Token name Token 名称
Symbol String The symbol for the token is deployed by default. Token 符号
Decimal Number decimal 小数位 18
Total Supply Number Total supply 总供应量 100,000,000
Token Owner L2 Address L2 Address that holds all the minted token Multisig address supported 用于持有发行出来的token可以准备一个多签地址
Token icon image

Address Configuration

1) System Admin L1 Address 需要生成多签地址

2) Proposer Challenger L1 Address 可以复用 System Admin L1 Address

Item Type Description 中文补充描述 Value
System Admin L1 Address Address that will own all ownable contracts on L1 once the deployment is finished, including the ProxyAdmin contract. aka finalSystemOwner. 管理l1的合约 使用多签地址
L2 Proxy Admin L2 Address Address that will own the ProxyAdmin contract on L2. The L2 ProxyAdmin contract owns all of the Proxy contracts for every predeployed contract in the range 0x42…0000 to 0x42..2048. This makes predeployed contracts easily upgradeable. aka proxyAdminOwner. 管理l2的合约可以使用多签地址 使用多签地址
Base Fee Recipient L1 Address L1 address that the base fees from all transactions on the L2 can be withdrawn to. aka baseFeeVaultRecipient. Base fee的收款账户
Tip Fee Recipient L1 Address L1 address that the tip fees from all transactions on the L2 can be withdrawn to. aka sequencerFeeVaultRecipient. 矿工费收款账户
L1 Data Fee Recipient L1 Address L1 address that the L1 data fees from all transactions on the L2 can be withdrawn to. aka l1FeeVaultRecipient. 数据由l2提交到l1的收费,这笔收费接受的账户
Proposer Challenger L1 Address Address that is allowed to challenge output proposals submitted to the L2OutputOracle. aka l2OutputOracleChallenger. 可以删除withdraw index可以使用多签,例如NR占有⅖席位,可以直接操作
portalGuardian L1 Address Address that has the ability to pause and unpause withdrawals 可以停止withdraw (和Challenger共用一个地址)