PageSourceSearch

https://ethpandaops.io/assets/js/45e17d27.e0c66272.js

js ethpandaops.io collected 2026-10-03 21:47:06 UTC 16,845 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkpublic_docs=globalThis.webpackChunkpublic_docs||[]).push([[4912],{28453(e,t,a){a.d(t,{R:()=>o,x:()=>r});var s=a(96540);const n={},i=s.createContext(n);function o(e){const t=s.useContext(i);return s.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:o(e.components),s.createElement(i.Provider,{value:t},e.children)}},60881(e){e.exports=JSON.parse('{"permalink":"/posts/kurtosis-l2","source":"@site/blog/2024-06-17-kurtosis-l2/index.md","title":"Reusing the Kurtosis Ethereum-package as a base for your L2 devnet","description":"Learn how to utilise the ethereum-package for the foundation of sophisticated tooling and L2 devnets.","date":"2024-06-17T00:00:00.000Z","tags":[{"inline":true,"label":"kurtosis","permalink":"/posts/tags/kurtosis"},{"inline":true,"label":"ethereum-package","permalink":"/posts/tags/ethereum-package"},{"inline":true,"label":"optimism-package","permalink":"/posts/tags/optimism-package"},{"inline":true,"label":"L1","permalink":"/posts/tags/l-1"},{"inline":true,"label":"L2","permalink":"/posts/tags/l-2"},{"inline":true,"label":"testnet","permalink":"/posts/tags/testnet"},{"inline":true,"label":"devnet","permalink":"/posts/tags/devnet"}],"readingTime":6.92,"hasTruncateMarker":false,"authors":[{"name":"parithosh","title":"DevOps Engineer","bio":"DevOps Engineer","url":"https://github.com/parithosh","github":"https://github.com/parithosh","twitter":"https://x.com/parithosh_j","website":"https://parithosh.com/","imageURL":"/img/team/parithosh.jpeg","key":"parithosh","page":null},{"name":"barnabasbusa","title":"DevOps Engineer","bio":"DevOps Engineer","url":"https://github.com/barnabasbusa","github":"https://github.com/barnabasbusa","twitter":"https://x.com/BarnabasBusa","imageURL":"/img/team/barnabasbusa.jpeg","key":"barnabasbusa","page":null}],"frontMatter":{"slug":"kurtosis-l2","title":"Reusing the Kurtosis Ethereum-package as a base for your L2 devnet","authors":["parithosh","barnabasbusa"],"description":"Learn how to utilise the ethereum-package for the foundation of sophisticated tooling and L2 devnets.","tags":["kurtosis","ethereum-package","optimism-package","L1","L2","testnet","devnet"],"image":"/img/blog/kurtosis-l2.jpg","githubRepos":[{"name":"ethereum-package","url":"https://github.com/ethpandaops/ethereum-package"},{"name":"optimism-package","url":"https://github.com/ethpandaops/optimism-package"}],"relatedLinks":[{"name":"Kurtosis Deep Dive","url":"https://ethpandaops.io/posts/kurtosis-deep-dive/"},{"name":"Starlark","url":"https://github.com/bazelbuild/starlark"}]},"unlisted":false,"prevItem":{"title":"Xatu Consensus Layer P2P tables now available","permalink":"/posts/xatu-consensus-layer-p2p"},"nextItem":{"title":"Kurtosis: A Deep Dive to Local Devnets","permalink":"/posts/kurtosis-deep-dive"}}')},67311(e,t,a){a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>d,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var s=a(60881),n=a(74848),i=a(28453);const o={slug:"kurtosis-l2",title:"Reusing the Kurtosis Ethereum-package as a base for your L2 devnet",authors:["parithosh","barnabasbusa"],description:"Learn how to utilise the ethereum-package for the foundation of sophisticated tooling and L2 devnets.",tags:["kurtosis","ethereum-package","optimism-package","L1","L2","testnet","devnet"],image:"/img/blog/kurtosis-l2.jpg",githubRepos:[{name:"ethereum-package",url:"https://github.com/ethpandaops/ethereum-package"},{name:"optimism-package",url:"https://github.com/ethpandaops/optimism-package"}],relatedLinks:[{name:"Kurtosis Deep Dive",url:"https://ethpandaops.io/posts/kurtosis-deep-dive/"},{name:"Starlark",url:"https://github.com/bazelbuild/starlark"}]},r=void 0,l={authorsImageUrls:[void 0,void 0]},c=[{value:"Introduction",id:"introduction",level:2},{value:"Interacting with imported packages in Kurtosis",id:"interacting-with-imported-packages-in-kurtosis",level:2},{value:"Optimism package overview",id:"optimism-package-overview",level:2},{value:"Configuration examples",id:"configuration-examples",level:2},{value:"What are the benefits of using Kurtosis for tooling and L2 devnets?",id:"what-are-the-benefits-of-using-kurtosis-for-tooling-and-l2-devnets",level:2},{value:"What are the disadvantages of using Kurtosis for L2 devnets?",id:"what-are-the-disadvantages-of-using-kurtosis-for-l2-devnets",level:2},{value:"Conclusion",id:"conclusion",level:2}];function h(e){const t={a:"a",code:"code",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,i.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h2,{id:"introduction",children:"Introduction"}),"\n",(0,n.jsxs)(t.p,{children:["In our latest series, which started with a ",(0,n.jsx)(t.a,{href:"https://ethpandaops.io/posts/kurtosis-deep-dive/",children:"deep dive into Kurtosis"}),", we've explored the utility of creating localized Ethereum devnets. Today, we expand on this by demonstrating how the ",(0,n.jsx)(t.a,{href:"https://github.com/ethpandaops/ethereum-package",children:"ethereum-package"})," can serve as the foundation for sophisticated tooling as well as L2 devnets."]}),"\n",(0,n.jsxs)(t.p,{children:["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 ",(0,n.jsx)(t.a,{href:"https://optimism.io",children:"Optimism"})," stack and create the ",(0,n.jsx)(t.a,{href:"https://github.com/ethpandaops/optimism-package",children:"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."]}
1),"\n",(0,n.jsx)(t.h2,{id:"interacting-with-imported-packages-in-kurtosis",children:"Interacting with imported packages in Kurtosis"}),"\n",(0,n.jsxs)(t.p,{children:["Kurtosis employs ",(0,n.jsx)(t.a,{href:"https://github.com/bazelbuild/starlark",children:"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 ",(0,n.jsx)(t.code,{children:"constants.star"}),") and another that uses these constants (defined in ",(0,n.jsx)(t.code,{children:"main.star"}),")."]}),"\n",(0,n.jsxs)(t.p,{children:["The ",(0,n.jsx)(t.code,{children:"main.star"})," file can then import the constants defined in ",(0,n.jsx)(t.code,{children:"constants.star"})," as follows:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-python",children:'constants = import_module("../constants/constants.star")\n'})}),"\n",(0,n.jsx)(t.p,{children:"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:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-python",children:'ethereum_package = import_module("github.com/ethpandaops/ethereum-package/main.star")\n'})}),"\n",(0,n.jsxs)(t.p,{children:["The ",(0,n.jsx)(t.code,{children:"ethereum-package"})," contains a ",(0,n.jsx)(t.code,{children:"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 ",(0,n.jsx)(t.code,{children:"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:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-python",children:"# Run the Ethereum devnet and store the context in a variable called l1\nl1 = ethereum_package.run(plan, ethereum_args)\n# Read the RPC URL of the first participant in the Ethereum devnet and store it in a variable called l1_rpc\nl1_rpc = l1.all_participants[0].el_context.rpc_http_url\n"})}),"\n",(0,n.jsxs)(t.p,{children:["We additionally contain prefunded accounts in the ",(0,n.jsx)(t.code,{children:"ethereum-package"})," that can be used for transactions on the base chain. These funds can be accessed by calling the ",(0,n.jsx)(t.code,{children:"pre_funded_accounts"})," in the ",(0,n.jsx)(t.code,{children:"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 ",(0,n.jsx)(t.a,{href:"https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#pre-funded-accounts-at-genesis",children:"here"}),". The code to access the private key of the 12th account would look like this:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-python",children:"l1_priv_key = l1.pre_funded_accounts[12].private_key  # reserved for L2 contract deployer\n"})}),"\n",(0,n.jsxs)(t.p,{children:["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 ",(0,n.jsx)(t.code,{children:"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 ",(0,n.jsx)(t.code,{children:"ethereum-package"})," can be found in this ",(0,n.jsx)(t.a,{href:"https://github.com/ethpandaops/optimism-package/blob/main/main.star",children:"main.star file"}),"."]}),"\n",(0,n.jsx)(t.h2,{id:"optimism-package-overview",children:"Optimism package overview"}),"\n",(0,n.jsxs)(t.p,{children:["The optimism docs contain a page on all the requirements to create a L2 rollup testnet, the docs can be found ",(0,n.jsx)(t.a,{href:"https://docs.optimism.io/builders/chain-operators/tutorials/create-l2-rollup",children:"here"}),". At a high level, the ",(0,n.jsx)(t.code,{children:"optimism-package"})," deploys the following:"]}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"Smart contracts"}),"\n",(0,n.jsx)(t.li,{children:"Sequencer node (consensus and execution client)"}),"\n",(0,n.jsx)(t.li,{children:"Batcher"}),"\n",(0,n.jsx)(t.li,{children:"Proposer"}),"\n"]}),"\n",(0,n.jsxs)(t.p,{children:["Once we imported the ",(0,n.jsx)(t.code,{children:"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 ",(0,n.jsx)(t.code,{children:"contract_deployer.star"})," module. This module contains all the logic for deploying and configuring the optimism contracts. The ",(0,n.jsx)(t.code,{children:"contract_deployer"})," module waits until the L1 in finalized to avoid issues and then proceeds with using ",(0,n.jsx)(t.code,{children:"cast"})," and ",(0,n.jsx)(t.code,{children:"forge"})," to deploy the contracts and using the ",(0,n.jsx)(t.code,{children:"op-node genesis"})," command to create the L2 genesis files."]}),"\n",(0,n.jsxs)(t.p,{children:["The L2 chain genesis files are then available in the ",(0,n.jsx)(t.code,{children:"contract_deployer"})," context, accessible via ",(0,n.jsx)(t.code,{children:"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 ",(0,n.jsx)(t.code,{children:"participant_network.star"}),".  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."]}),"\n",(0,n.jsxs)(t.p,{children:["Once the nodes, batcher and proposer are setup, the ",(0,n.jsx)(t.code,{children:"optimism-package"}
1)," will then turn to setting up tooling. This tooling currently includes just ",(0,n.jsx)(t.code,{children:"op-blockscout"}),", but can be expanded to include any other tooling that is required. The ",(0,n.jsx)(t.code,{children:"op-blockscout"})," is a fork of the original blockscout that is configured to work with the L2 network. The ",(0,n.jsx)(t.code,{children:"op-blockscout"})," is another example of how extensible the module approach is, it references the L1 RPC from the ",(0,n.jsx)(t.code,{children:"ethereum-package"})," as well as the L2 RPC from the ",(0,n.jsx)(t.code,{children:"optimism-package"})," to provide a seamless experience for the developer."]}),"\n",(0,n.jsxs)(t.p,{children:["The result of the ",(0,n.jsx)(t.code,{children:"optimism-package"})," execution is a fully functional L2 devnet as well as a list of services and URLs to access them by."]}),"\n",(0,n.jsx)(t.h2,{id:"configuration-examples",children:"Configuration examples"}),"\n",(0,n.jsxs)(t.p,{children:["As the ",(0,n.jsx)(t.code,{children:"optimism-package"})," is designed to fully encompass the ethereum-package, its configuration file seamlessly integrates all parameters from the ",(0,n.jsx)(t.code,{children:"ethereum-package"})," alongside its own unique settings. To differentiate from using the original ",(0,n.jsx)(t.code,{children:"ethereum-package"})," a new field is introduced in the configuration file called ",(0,n.jsx)(t.code,{children:"optimism_package"}),". This field will contain all the new configuration parameters that are specific to the L2 devnet."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-yaml",children:"optimism_package: # parameters specific to the optimism package\n  participants:\n    - el_type: op-geth\n      cl_type: op-node\n  additional_services:\n    - blockscout\nethereum_package: # inherited from the ethereum package\n  participants:\n    - el_type: geth\n    - el_type: reth\n  network_params:\n    preset: minimal\n  additional_services:\n    - dora\n    - blockscout\n"})}),"\n",(0,n.jsx)(t.h2,{id:"what-are-the-benefits-of-using-kurtosis-for-tooling-and-l2-devnets",children:"What are the benefits of using Kurtosis for tooling and L2 devnets?"}),"\n",(0,n.jsxs)(t.p,{children:["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 ",(0,n.jsx)(t.a,{href:"https://docs.kurtosis.com/advanced-concepts/enclaves/",children:"enclaves"}),", these enclaves are fully isolated from each other - allowing multiple tests to run in parallel without interference or networking issues."]}),"\n",(0,n.jsx)(t.h2,{id:"what-are-the-disadvantages-of-using-kurtosis-for-l2-devnets",children:"What are the disadvantages of using Kurtosis for L2 devnets?"}),"\n",(0,n.jsx)(t.p,{children:"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."}),"\n",(0,n.jsx)(t.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,n.jsxs)(t.p,{children:["To fully appreciate the power and flexibility of using Kurtosis with ",(0,n.jsx)(t.code,{children:"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!"]})]})}function d(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.