Commit f487bec7 authored by Yann Hodique's avatar Yann Hodique Committed by GitHub

feat(kurtosis-devnet): main tool for deploying devnets (#13525)

This ties together the various capabilities in pkg/ and orchestrates them. The phases are:
- build local artifacts per input template specification
- put them where they can be accessed at runtime by kurtosis
- run the deployment
- collect useful information and output it for downstream consumption

Also provide sample devnet definitions

Those are just starting points. We'll need to come up with definitions
that actually make sense.

Note that there is a temporary workaround for op-deployer image:

We need http locator support, which has been merged into op-deployer,
but the current version at HEAD doesn't quite work with kurtosis yet.

We'll fix that separately, at which point we'll be able to point to a
localDockerBuild of "op-deployer" just as well, if we want to.
parent ca889438
# Kurtosis-devnet support
## devnet specification
Due to sandboxing issues across repositories, we currently rely on a slight
superset of the native optimism-package specification YAML file, via go
templates.
So that means in particular that the regular optimism-package input is valid
here.
Additional custom functions:
- localDockerImage(PROJECT): builds a docker image for PROJECT based on the
current branch content.
- localContractArtifacts(LAYER): builds a contracts bundle based on the current
branch content (note: LAYER is currently ignored, we might need to revisit)
Example:
```yaml
...
op_contract_deployer_params:
image: {{ localDockerImage "op-deployer" }}
l1_artifacts_locator: {{ localContractArtifacts "l1" }}
l2_artifacts_locator: {{ localContractArtifacts "l2" }}
...
```
The list of supported PROJECT values can be found in `justfile` as a
PROJECT-image target. Adding a target there will immediately available to the
template engine.
## devnet deployment tool
Located in cmd/main.go, this tool handle the creation of an enclave matching the
provided specification.
The expected entry point for interacting with it is the corresponding
`just devnet SPEC` target.
This takes an optional 2nd argument, that can be used to provide values for the
template interpretation.
Note that a SPEC of the form `FOO.yaml` will yield a kurtosis enclave named
`FOO-devnet`
Convenience targets can be added to `justfile` for specific specifications, for
example:
```just
interop-devnet: (devnet "interop.yaml")
```
## devnet output
One important aspect of the devnet workflow is that the output should be
*consumable*. Going forward we want to integrate them into larger worfklows
(serving as targets for tests for example, or any other form of automation).
To address this, the deployment tool outputs a document with (hopefully!) useful
information. Here's a short extract:
```json
{
"l1": {
"name": "Ethereum",
"nodes": [
{
"cl": "http://localhost:53689",
"el": "http://localhost:53620"
}
]
},
"l2": [
{
"name": "op-kurtosis-1",
"id": "2151908",
"services": {
"batcher": "http://localhost:57259"
},
"nodes": [
{
"cl": "http://localhost:57029",
"el": "http://localhost:56781"
}
],
"addresses": {
"addressManager": "0x1b89c03f2d8041b2ba16b5128e613d9279195d1a",
...
}
},
...
],
"wallets": {
"baseFeeVaultRecipient": {
"address": "0xF435e3ba80545679CfC24E5766d7B02F0CCB5938",
"private_key": "0xc661dd5d4b091676d1a5f2b5110f9a13cb8682140587bd756e357286a98d2c26"
},
...
}
}
```
## further interactions
Beyond deployment, we can interact with enclaves normally.
In particular, cleaning up a devnet can be achieved using
`kurtosis rm FOO-devnet` and the likes.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"path/filepath"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/build"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/serve"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/tmpl"
"github.com/urfave/cli/v2"
)
type config struct {
templateFile string
dataFile string
kurtosisPackage string
enclave string
environment string
dryRun bool
localHostName string
baseDir string
}
func newConfig(c *cli.Context) (*config, error) {
cfg := &config{
templateFile: c.String("template"),
dataFile: c.String("data"),
kurtosisPackage: c.String("kurtosis-package"),
enclave: c.String("enclave"),
environment: c.String("environment"),
dryRun: c.Bool("dry-run"),
localHostName: c.String("local-hostname"),
}
// Validate required flags
if cfg.templateFile == "" {
return nil, fmt.Errorf("template file is required")
}
cfg.baseDir = filepath.Dir(cfg.templateFile)
return cfg, nil
}
type staticServer struct {
dir string
*serve.Server
}
func launchStaticServer(ctx context.Context, cfg *config) (*staticServer, func(), error) {
// we will serve content from this tmpDir for the duration of the devnet creation
tmpDir, err := os.MkdirTemp("", "contracts-bundle")
if err != nil {
return nil, nil, fmt.Errorf("error creating temporary directory: %w", err)
}
server := serve.NewServer(
serve.WithStaticDir(tmpDir),
serve.WithHostname(cfg.localHostName),
)
if err := server.Start(ctx); err != nil {
return nil, nil, fmt.Errorf("error starting server: %w", err)
}
return &staticServer{
dir: tmpDir,
Server: server,
}, func() {
if err := server.Stop(ctx); err != nil {
log.Printf("Error stopping server: %v\n", err)
}
if err := os.RemoveAll(tmpDir); err != nil {
log.Printf("Error removing temporary directory: %v\n", err)
}
}, nil
}
func localDockerImageOption(cfg *config) tmpl.TemplateContextOptions {
dockerBuilder := build.NewDockerBuilder(
build.WithDockerBaseDir(cfg.baseDir),
build.WithDockerDryRun(cfg.dryRun),
)
imageTag := func(projectName string) string {
return fmt.Sprintf("%s:%s", projectName, cfg.enclave)
}
return tmpl.WithFunction("localDockerImage", func(projectName string) (string, error) {
return dockerBuilder.Build(projectName, imageTag(projectName))
})
}
func localContractArtifactsOption(cfg *config, server *staticServer) tmpl.TemplateContextOptions {
contractsBundle := fmt.Sprintf("contracts-bundle-%s.tar.gz", cfg.enclave)
contractsBundlePath := func(_ string) string {
return filepath.Join(server.dir, contractsBundle)
}
contractBuilder := build.NewContractBuilder(
build.WithContractBaseDir(cfg.baseDir),
build.WithContractDryRun(cfg.dryRun),
)
return tmpl.WithFunction("localContractArtifacts", func(layer string) (string, error) {
bundlePath := contractsBundlePath(layer)
// we're in a temp dir, so we can skip the build if the file already
// exists: it'll be the same file! In particular, since we're ignoring
// layer for now, skip the 2nd build.
if _, err := os.Stat(bundlePath); err != nil {
if err := contractBuilder.Build(layer, bundlePath); err != nil {
return "", err
}
}
url := fmt.Sprintf("%s/%s", server.URL(), contractsBundle)
log.Printf("%s: contract artifacts available at: %s\n", layer, url)
return url, nil
})
}
func renderTemplate(cfg *config, server *staticServer) (*bytes.Buffer, error) {
opts := []tmpl.TemplateContextOptions{
localDockerImageOption(cfg),
localContractArtifactsOption(cfg, server),
}
// Read and parse the data file if provided
if cfg.dataFile != "" {
data, err := os.ReadFile(cfg.dataFile)
if err != nil {
return nil, fmt.Errorf("error reading data file: %w", err)
}
var templateData map[string]interface{}
if err := json.Unmarshal(data, &templateData); err != nil {
return nil, fmt.Errorf("error parsing JSON data: %w", err)
}
opts = append(opts, tmpl.WithData(templateData))
}
// Open template file
tmplFile, err := os.Open(cfg.templateFile)
if err != nil {
return nil, fmt.Errorf("error opening template file: %w", err)
}
defer tmplFile.Close()
// Create template context
tmplCtx := tmpl.NewTemplateContext(opts...)
// Process template
buf := bytes.NewBuffer(nil)
if err := tmplCtx.InstantiateTemplate(tmplFile, buf); err != nil {
return nil, fmt.Errorf("error processing template: %w", err)
}
return buf, nil
}
func deploy(ctx context.Context, cfg *config, r io.Reader) error {
// Create a multi reader to output deployment input to stdout
buf := bytes.NewBuffer(nil)
tee := io.TeeReader(r, buf)
// Log the deployment input
log.Println("Deployment input:")
if _, err := io.Copy(os.Stdout, tee); err != nil {
return fmt.Errorf("error copying deployment input: %w", err)
}
kurtosisDeployer := kurtosis.NewKurtosisDeployer(
kurtosis.WithKurtosisBaseDir(cfg.baseDir),
kurtosis.WithKurtosisDryRun(cfg.dryRun),
kurtosis.WithKurtosisPackageName(cfg.kurtosisPackage),
kurtosis.WithKurtosisEnclave(cfg.enclave),
)
env, err := kurtosisDeployer.Deploy(ctx, buf)
if err != nil {
return fmt.Errorf("error deploying kurtosis: %w", err)
}
envOutput := os.Stdout
if cfg.environment != "" {
envOutput, err = os.Create(cfg.environment)
if err != nil {
return fmt.Errorf("error creating environment file: %w", err)
}
defer envOutput.Close()
} else {
log.Println("Environment description:")
}
enc := json.NewEncoder(envOutput)
enc.SetIndent("", " ")
if err := enc.Encode(env); err != nil {
return fmt.Errorf("error encoding environment: %w", err)
}
return nil
}
func mainFunc(cfg *config) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server, cleanup, err := launchStaticServer(ctx, cfg)
if err != nil {
return fmt.Errorf("error launching static server: %w", err)
}
defer cleanup()
buf, err := renderTemplate(cfg, server)
if err != nil {
return fmt.Errorf("error rendering template: %w", err)
}
return deploy(ctx, cfg, buf)
}
func mainAction(c *cli.Context) error {
cfg, err := newConfig(c)
if err != nil {
return fmt.Errorf("error parsing config: %w", err)
}
return mainFunc(cfg)
}
func getFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "template",
Usage: "Path to the template file (required)",
Required: true,
},
&cli.StringFlag{
Name: "data",
Usage: "Path to JSON data file (optional)",
},
&cli.StringFlag{
Name: "kurtosis-package",
Usage: "Kurtosis package to deploy (optional)",
Value: kurtosis.DefaultPackageName,
},
&cli.StringFlag{
Name: "enclave",
Usage: "Enclave name (optional)",
Value: kurtosis.DefaultEnclave,
},
&cli.StringFlag{
Name: "environment",
Usage: "Path to JSON environment file output (optional)",
},
&cli.BoolFlag{
Name: "dry-run",
Usage: "Dry run mode (optional)",
},
&cli.StringFlag{
Name: "local-hostname",
Usage: "DNS for localhost from Kurtosis perspective (optional)",
Value: "host.docker.internal",
},
}
}
func main() {
app := &cli.App{
Name: "kurtosis-devnet",
Usage: "Deploy and manage Optimism devnet using Kurtosis",
Flags: getFlags(),
Action: mainAction,
}
if err := app.Run(os.Args); err != nil {
log.Fatalf("Error: %v\n", err)
}
}
package main
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
func TestParseFlags(t *testing.T) {
tests := []struct {
name string
args []string
wantCfg *config
wantError bool
}{
{
name: "valid configuration",
args: []string{
"--template", "path/to/template.yaml",
"--enclave", "test-enclave",
"--local-hostname", "test.local",
},
wantCfg: &config{
templateFile: "path/to/template.yaml",
enclave: "test-enclave",
localHostName: "test.local",
kurtosisPackage: kurtosis.DefaultPackageName,
},
wantError: false,
},
{
name: "missing required template",
args: []string{"--enclave", "test-enclave"},
wantCfg: nil,
wantError: true,
},
{
name: "with data file",
args: []string{
"--template", "path/to/template.yaml",
"--data", "path/to/data.json",
},
wantCfg: &config{
templateFile: "path/to/template.yaml",
dataFile: "path/to/data.json",
localHostName: "host.docker.internal",
enclave: kurtosis.DefaultEnclave,
kurtosisPackage: kurtosis.DefaultPackageName,
},
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg *config
app := &cli.App{
Flags: getFlags(),
Action: func(c *cli.Context) (err error) {
cfg, err = newConfig(c)
return
},
}
// Prepend program name to args as urfave/cli expects
args := append([]string{"prog"}, tt.args...)
err := app.Run(args)
if tt.wantError {
assert.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, cfg)
assert.Equal(t, tt.wantCfg.templateFile, cfg.templateFile)
assert.Equal(t, tt.wantCfg.enclave, cfg.enclave)
assert.Equal(t, tt.wantCfg.localHostName, cfg.localHostName)
assert.Equal(t, tt.wantCfg.kurtosisPackage, cfg.kurtosisPackage)
if tt.wantCfg.dataFile != "" {
assert.Equal(t, tt.wantCfg.dataFile, cfg.dataFile)
}
})
}
}
func TestLaunchStaticServer(t *testing.T) {
cfg := &config{
localHostName: "test.local",
}
ctx := context.Background()
server, cleanup, err := launchStaticServer(ctx, cfg)
require.NoError(t, err)
defer cleanup()
// Verify server properties
assert.NotEmpty(t, server.dir)
assert.DirExists(t, server.dir)
assert.NotNil(t, server.Server)
// Verify cleanup works
cleanup()
assert.NoDirExists(t, server.dir)
}
func TestRenderTemplate(t *testing.T) {
// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "template-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create a test template file
templateContent := `
name: {{.name}}
image: {{localDockerImage "test-project"}}
artifacts: {{localContractArtifacts "l1"}}`
templatePath := filepath.Join(tmpDir, "template.yaml")
err = os.WriteFile(templatePath, []byte(templateContent), 0644)
require.NoError(t, err)
// Create a test data file
dataContent := `{"name": "test-deployment"}`
dataPath := filepath.Join(tmpDir, "data.json")
err = os.WriteFile(dataPath, []byte(dataContent), 0644)
require.NoError(t, err)
cfg := &config{
templateFile: templatePath,
dataFile: dataPath,
enclave: "test-enclave",
dryRun: true, // Important for tests
}
ctx := context.Background()
server, cleanup, err := launchStaticServer(ctx, cfg)
require.NoError(t, err)
defer cleanup()
buf, err := renderTemplate(cfg, server)
require.NoError(t, err)
// Verify template rendering
assert.Contains(t, buf.String(), "test-deployment")
assert.Contains(t, buf.String(), "test-project:test-enclave")
assert.Contains(t, buf.String(), server.URL())
}
func TestDeploy(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create a temporary directory for the environment output
tmpDir, err := os.MkdirTemp("", "deploy-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
envPath := filepath.Join(tmpDir, "env.json")
cfg := &config{
environment: envPath,
dryRun: true,
}
// Create a simple deployment configuration
deployConfig := bytes.NewBufferString(`{"test": "config"}`)
err = deploy(ctx, cfg, deployConfig)
require.NoError(t, err)
// Verify the environment file was created
assert.FileExists(t, envPath)
// Read and verify the content
content, err := os.ReadFile(envPath)
require.NoError(t, err)
var env map[string]interface{}
err = json.Unmarshal(content, &env)
require.NoError(t, err)
}
// TestMainFunc performs an integration test of the main function
func TestMainFunc(t *testing.T) {
// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "main-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create test template
templatePath := filepath.Join(tmpDir, "template.yaml")
err = os.WriteFile(templatePath, []byte("name: test"), 0644)
require.NoError(t, err)
// Create environment output path
envPath := filepath.Join(tmpDir, "env.json")
cfg := &config{
templateFile: templatePath,
environment: envPath,
dryRun: true,
}
err = mainFunc(cfg)
require.NoError(t, err)
// Verify the environment file was created
assert.FileExists(t, envPath)
}
optimism_package:
chains:
- participants:
- el_type: op-geth
el_image: ""
el_log_level: ""
el_extra_env_vars: {}
el_extra_labels: {}
el_extra_params: []
el_tolerations: []
el_volume_size: 0
el_min_cpu: 0
el_max_cpu: 0
el_min_mem: 0
el_max_mem: 0
cl_type: op-node
cl_image: {{ localDockerImage "op-node" }}
cl_log_level: ""
cl_extra_env_vars: {}
cl_extra_labels: {}
cl_extra_params: []
cl_tolerations: []
cl_volume_size: 0
cl_min_cpu: 0
cl_max_cpu: 0
cl_min_mem: 0
cl_max_mem: 0
node_selectors: {}
tolerations: []
count: 1
network_params:
network: "kurtosis"
network_id: "2151908"
seconds_per_slot: 2
name: "op-kurtosis-1"
fjord_time_offset: 0
granite_time_offset: 0
holocene_time_offset: 0
fund_dev_accounts: true
batcher_params:
image: {{ localDockerImage "op-batcher" }}
extra_params: []
mev_params:
rollup_boost_image: ""
builder_host: ""
builder_port: ""
additional_services: []
- participants:
- el_type: op-geth
el_image: ""
el_log_level: ""
el_extra_env_vars: {}
el_extra_labels: {}
el_extra_params: []
el_tolerations: []
el_volume_size: 0
el_min_cpu: 0
el_max_cpu: 0
el_min_mem: 0
el_max_mem: 0
cl_type: op-node
cl_image: {{ localDockerImage "op-node" }}
cl_log_level: ""
cl_extra_env_vars: {}
cl_extra_labels: {}
cl_extra_params: []
cl_tolerations: []
cl_volume_size: 0
cl_min_cpu: 0
cl_max_cpu: 0
cl_min_mem: 0
cl_max_mem: 0
node_selectors: {}
tolerations: []
count: 1
network_params:
network: "kurtosis"
network_id: "2151909"
seconds_per_slot: 2
name: "op-kurtosis-2"
fjord_time_offset: 0
granite_time_offset: 0
holocene_time_offset: 0
fund_dev_accounts: true
batcher_params:
image: {{ localDockerImage "op-batcher" }}
extra_params: []
mev_params:
rollup_boost_image: ""
builder_host: ""
builder_port: ""
additional_services: []
op_contract_deployer_params:
image: opsigma/op-deployer:v0.0.7-http
l1_artifacts_locator: {{ localContractArtifacts "l1" }}
l2_artifacts_locator: {{ localContractArtifacts "l2" }}
global_log_level: "info"
global_node_selectors: {}
global_tolerations: []
persistent: false
ethereum_package:
network_params:
preset: minimal
genesis_delay: 5
additional_preloaded_contracts: |
{
"0x4e59b44847b379578588920cA78FbF26c0B4956C": {
"balance": "0ETH",
"code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3",
"storage": {},
"nonce": "1"
}
}
...@@ -30,3 +30,18 @@ op-conductor-image TAG='op-conductor:devnet': (_docker_build_stack TAG "op-condu ...@@ -30,3 +30,18 @@ op-conductor-image TAG='op-conductor:devnet': (_docker_build_stack TAG "op-condu
da-server-image TAG='da-server:devnet': (_docker_build_stack TAG "da-server-target") da-server-image TAG='da-server:devnet': (_docker_build_stack TAG "da-server-target")
op-supervisor-image TAG='op-supervisor:devnet': (_docker_build_stack TAG "op-supervisor-target") op-supervisor-image TAG='op-supervisor:devnet': (_docker_build_stack TAG "op-supervisor-target")
op-deployer-image TAG='op-deployer:devnet': (_docker_build_stack TAG "op-deployer-target") op-deployer-image TAG='op-deployer:devnet': (_docker_build_stack TAG "op-deployer-target")
# Devnet template recipe
devnet TEMPLATE_FILE DATA_FILE="":
go run cmd/main.go -template "{{TEMPLATE_FILE}}" -data "{{DATA_FILE}}" -enclave `basename {{TEMPLATE_FILE}} .yaml`-devnet
# Devnet recipes
# Mini devnet
mini-devnet: (devnet "mini.yaml")
# Simple devnet
simple-devnet: (devnet "simple.yaml")
# Interop devnet
interop-devnet: (devnet "interop.yaml")
optimism_package:
chains:
- participants:
- el_type: op-geth
el_image: ""
el_log_level: ""
el_extra_env_vars: {}
el_extra_labels: {}
el_extra_params: []
el_tolerations: []
el_volume_size: 0
el_min_cpu: 0
el_max_cpu: 0
el_min_mem: 0
el_max_mem: 0
cl_type: op-node
cl_image: ""
cl_log_level: ""
cl_extra_env_vars: {}
cl_extra_labels: {}
cl_extra_params: []
cl_tolerations: []
cl_volume_size: 0
cl_min_cpu: 0
cl_max_cpu: 0
cl_min_mem: 0
cl_max_mem: 0
node_selectors: {}
tolerations: []
count: 1
network_params:
network: "kurtosis"
network_id: "2151908"
seconds_per_slot: 2
name: "op-kurtosis"
fjord_time_offset: 0
granite_time_offset: 0
holocene_time_offset: 0
fund_dev_accounts: true
batcher_params:
image: ""
extra_params: []
mev_params:
rollup_boost_image: ""
builder_host: ""
builder_port: ""
additional_services: []
op_contract_deployer_params:
image: opsigma/op-deployer:v0.0.7-http
l1_artifacts_locator: https://storage.googleapis.com/oplabs-contract-artifacts/artifacts-v1-9af7366a7102f51e8dbe451dcfa22971131d89e218915c91f420a164cc48be65.tar.gz
l2_artifacts_locator: https://storage.googleapis.com/oplabs-contract-artifacts/artifacts-v1-9af7366a7102f51e8dbe451dcfa22971131d89e218915c91f420a164cc48be65.tar.gz
global_log_level: "info"
global_node_selectors: {}
global_tolerations: []
persistent: false
ethereum_package:
network_params:
preset: minimal
genesis_delay: 5
additional_preloaded_contracts: |
{
"0x4e59b44847b379578588920cA78FbF26c0B4956C": {
"balance": "0ETH",
"code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3",
"storage": {},
"nonce": "1"
}
}
optimism_package:
chains:
- participants:
- el_type: op-geth
el_image: ""
el_log_level: ""
el_extra_env_vars: {}
el_extra_labels: {}
el_extra_params: []
el_tolerations: []
el_volume_size: 0
el_min_cpu: 0
el_max_cpu: 0
el_min_mem: 0
el_max_mem: 0
cl_type: op-node
cl_image: ""
cl_log_level: ""
cl_extra_env_vars: {}
cl_extra_labels: {}
cl_extra_params: []
cl_tolerations: []
cl_volume_size: 0
cl_min_cpu: 0
cl_max_cpu: 0
cl_min_mem: 0
cl_max_mem: 0
node_selectors: {}
tolerations: []
count: 1
network_params:
network: "kurtosis"
network_id: "2151908"
seconds_per_slot: 2
name: "op-kurtosis"
fjord_time_offset: 0
granite_time_offset: 0
holocene_time_offset: 0
fund_dev_accounts: true
batcher_params:
image: ""
extra_params: []
mev_params:
rollup_boost_image: ""
builder_host: ""
builder_port: ""
additional_services: []
op_contract_deployer_params:
image: opsigma/op-deployer:v0.0.7-http
l1_artifacts_locator: {{ localContractArtifacts "l1" }}
l2_artifacts_locator: {{ localContractArtifacts "l2" }}
global_log_level: "info"
global_node_selectors: {}
global_tolerations: []
persistent: false
ethereum_package:
network_params:
preset: minimal
genesis_delay: 5
additional_preloaded_contracts: |
{
"0x4e59b44847b379578588920cA78FbF26c0B4956C": {
"balance": "0ETH",
"code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3",
"storage": {},
"nonce": "1"
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment