Skip to main content

Reusing the Kurtosis Ethereum-package as a base for your L2 devnet

·1299 words·7 mins·
Kurtosis Ethereum-Package Optimism-Package L2 L1 Devnet Testnet
DevOps Engineer
DevOps Engineer
Table of Contents


In our latest series, which started with a deep dive into Kurtosis, we’ve explored the utility of creating localized Ethereum devnets. Today, we expand on this by demonstrating how the ethereum-package can serve as the foundation for sophisticated tooling as well as L2 devnets.

The advantage of a package based approach on Kurtosis, is that packages can be imported and linked in other packages. This means that one can create a new package that imports the ethereum-package and expect that a base layer Ethereum will always be spun up with any minimal maintenance effort. This feature would be especially useful for anyone creating tooling that isn’t strictly focussed on the Ethereum base layer, but still relies on it for data or state access. In order to expand on how this feature could be used, we decided on building an example that runs a L2 devnet but with the base layer being Ethereum, imported as a package. For this example, we will use the Optimism stack and create the optimism-package purely due to the fact that it reuses many base layer components we already had definitions for. There is however no reason that the example could not apply to any other L2 or tool that relies on Ethereum as a base layer.

Interacting with imported packages in Kurtosis

Kurtosis employs Starlark for its scripting needs, allowing you to treat each folder as a module that can be conveniently imported into other modules, enhancing modularity and reuse. Let’s take an example of two modules, one that defines the constants (defined in and another that uses these constants (defined in

The file can then import the constants defined in as follows:

constants = import_module("../constants/")

In the same way that we imported a file from a module into another module, we can also import an entire package from github into any arbitrary module. This would look like this:

ethereum_package = import_module("")

The ethereum-package contains a run function that needs to be called with the arguments used to start up the Ethereum devnet. The response is stored in a variable called l1, this variable contains the context of the Ethereum devnet. We can then reference any variable stored in this context, for e.g we can obtain the RPC URL of an Ethereum node in order to interact with the underlying chain. The code to do so would look like this:

# Run the Ethereum devnet and store the context in a variable called l1
l1 =, ethereum_args)
# Read the RPC URL of the first participant in the Ethereum devnet and store it in a variable called l1_rpc
l1_rpc = l1.all_participants[0].el_context.rpc_http_url

We additionally contain prefunded accounts in the ethereum-package that can be used for transactions on the base chain. These funds can be accessed by calling the pre_funded_accounts in the ethereum-package context. We do reserve certain accounts for specific purposes, for example, the 12th account is reserved for the L2 contract deployer. A full list of pre-allocations can be found here. The code to access the private key of the 12th account would look like this:

l1_priv_key = l1.pre_funded_accounts[12].private_key  # reserved for L2 contract deployer

The Ethereum RPC as well as a private key with funds would be integral components for building any tooling that interacts with the Ethereum base layer, in the case of the optimism-package, we pass these values on to the optimism contract deployer as well as the op-nodes that are spun up later in the process. A full example of how we’ve interacted with the base ethereum-package can be found in this file.

Optimism package overview

The optimism docs contain a page on all the requirements to create a L2 rollup testnet, the docs can be found here. At a high level, the optimism-package deploys the following:

  • Smart contracts
  • Sequencer node (consensus and execution client)
  • Batcher
  • Proposer

Once we imported the ethereum-package and possess the L1 context, we pass on the required variables such as a prefunded privatekey, RPC, chainID and a few other values to the module. This module contains all the logic for deploying and configuring the optimism contracts. The contract_deployer module waits until the L1 in finalized to avoid issues and then proceeds with using cast and forge to deploy the contracts and using the op-node genesis command to create the L2 genesis files.

The L2 chain genesis files are then available in the contract_deployer context, accessible via op_genesis.files_artifacts[0] for use by the nodes. The package then continues on to launching the L2 network participants, this logic is encapsulated in the module The module launches all the participants in the L2 network, including the sequencer, batcher, proposer and the op-nodes. If there are already setup nodes on the L2 network, then the module will connect the new nodes to the existing nodes.

Once the nodes, batcher and proposer are setup, the optimism-package will then turn to setting up tooling. This tooling currently includes just op-blockscout, but can be expanded to include any other tooling that is required. The op-blockscout is a fork of the original blockscout that is configured to work with the L2 network. The op-blockscout is another example of how extensible the module approach is, it references the L1 RPC from the ethereum-package as well as the L2 RPC from the optimism-package to provide a seamless experience for the developer.

The result of the optimism-package execution is a fully functional L2 devnet as well as a list of services and URLs to access them by.

Configuration examples

As the optimism-package is designed to fully encompass the ethereum-package, its configuration file seamlessly integrates all parameters from the ethereum-package alongside its own unique settings. To differentiate from using the original ethereum-package a new field is introduced in the configuration file called optimism_package. This field will contain all the new configuration parameters that are specific to the L2 devnet.

optimism_package: # parameters specific to the optimism package
    - el_type: op-geth
      cl_type: op-node
    - blockscout
ethereum_package: # inherited from the ethereum package
    - el_type: geth
    - el_type: reth
    preset: minimal
    - dora
    - blockscout

What are the benefits of using Kurtosis for tooling and L2 devnets?

The current way of creating any L2 devnet/tooling is to use an existing L1 testnet and deploy the L2 package/tooling on top of it or to use custom bash scripts to perform local testing. This is a cumbersome process and still requires a considerable amount of maintenance effort to ensure the L1 works as expected through upgrades. The Kurtosis approach allows you to create a local L1 devnet in a few minutes with relatively little maintenance effort and allows you to spend more time on the L2 devnet/tooling - enabling faster and safer prototyping. Kurtosis also works under the concept of enclaves, these enclaves are fully isolated from each other - allowing multiple tests to run in parallel without interference or networking issues.

What are the disadvantages of using Kurtosis for L2 devnets?

The main disadvantage of using Kurtosis is the local nature of the devnet. This means that providing devnet access to other developers is not as easy as using a public testnet, this is a fundamental bottleneck in how kurtosis works today. Additionally, the lack of default persistent storage in Kurtosis implies that you lose all your data when you stop the devnet(unless saved ahead of time). This is not a problem for testing purposes, but it is something to keep in mind.


To fully appreciate the power and flexibility of using Kurtosis with ethereum-package, we encourage you to initiate your own projects reusing it for tooling or to define your own L2. Explore the detailed documentation, experiment with the configurations, and join the discord to share your experiences!