From f59d5a762f6c4b09df0af69eafc250c5ac5ecd65 Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:43:43 +0000 Subject: [PATCH 01/10] feat: allow for devnet deployments against already instantiated devnet environments --- README.md | 9 + config/contexts/migrations/v0.1.1-v0.1.2.go | 49 +++ config/contexts/registry.go | 13 +- config/contexts/v0.1.2.yaml | 186 ++++++++++ go.mod | 4 + go.sum | 12 + pkg/commands/devnet.go | 5 + pkg/commands/devnet_actions.go | 317 ++++++++++-------- pkg/commands/transporter.go | 33 +- pkg/common/config.go | 1 + pkg/common/contract_caller.go | 24 +- pkg/common/devnet/constants.go | 10 +- pkg/common/devnet/funding.go | 102 ++++-- pkg/common/devnet/getters.go | 53 +++ pkg/common/devnet/utils.go | 56 ++++ pkg/common/getters.go | 24 ++ pkg/migration/migrator.go | 79 +++++ .../avs_context_0_1_1_to_0_1_2_test.go | 132 ++++++++ 18 files changed, 898 insertions(+), 211 deletions(-) create mode 100644 config/contexts/migrations/v0.1.1-v0.1.2.go create mode 100644 config/contexts/v0.1.2.yaml create mode 100644 test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go diff --git a/README.md b/README.md index 7dc9dbaf..02a4ba12 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,15 @@ DevNet management commands: | `stop --project.name` | Stops the specific project's devnet | | `stop --port` | Stops the specific port e.g.: `stop --port 8545` | + +Alternatively, `devnet` can be started against an already running forked enviorment (eg, local anvil instances or from remote BuildBear sandboxes). This will allow you to interact with a testnet without spinning up or spinning down the onchain environment each iteration. + +Start/create your own forked sepolia instances and in your project directory, set the appropriate `rpc_urls` in your context and run: + +```bash +devkit avs devnet start --skip-forking +``` + ### 7️⃣ Simulate Task Execution (`devkit avs call`) Triggers task execution through your AVS, simulating how a task would be submitted, processed, and validated. Useful for testing end-to-end behavior of your logic in a local environment. diff --git a/config/contexts/migrations/v0.1.1-v0.1.2.go b/config/contexts/migrations/v0.1.1-v0.1.2.go new file mode 100644 index 00000000..62e475ae --- /dev/null +++ b/config/contexts/migrations/v0.1.1-v0.1.2.go @@ -0,0 +1,49 @@ +package contextMigrations + +import ( + "strings" + + "github.com/Layr-Labs/devkit-cli/pkg/common/devnet" + "github.com/Layr-Labs/devkit-cli/pkg/migration" + "gopkg.in/yaml.v3" +) + +func Migration_0_1_1_to_0_1_2(user, old, new *yaml.Node) (*yaml.Node, error) { + // Update fork block heights to match ponos project + engine := migration.PatchEngine{} + if err := engine.Apply(); err != nil { + return nil, err + } + + // Get the contexts name + contextName := migration.ResolveNode(user, []string{"context", "name"}) + + // If contextName contains devnet, insert mnemonic + if strings.Contains(contextName.Value, devnet.DEVNET_CONTEXT) { + // Insert mnemonic into yaml after chains + _ = migration.InsertAfterKeyWithComment( + user, + []string{"context"}, + "chains", + "mnemonic", + &yaml.Node{ + Kind: yaml.ScalarNode, + Style: yaml.DoubleQuotedStyle, + Value: devnet.DEFAULT_MNEMONIC, + }, + "Devnet mnemonic for unlocked accounts", + false, + ) + } + + if err := engine.Apply(); err != nil { + return nil, err + } + + // Upgrade the version + if v := migration.ResolveNode(user, []string{"version"}); v != nil { + v.Value = "0.1.2" + } + + return user, nil +} diff --git a/config/contexts/registry.go b/config/contexts/registry.go index 3555ffef..36318f2c 100644 --- a/config/contexts/registry.go +++ b/config/contexts/registry.go @@ -15,7 +15,7 @@ import ( ) // Set the latest version -const LatestVersion = "0.1.1" +const LatestVersion = "0.1.2" // Array of default contexts to create in project var DefaultContexts = [...]string{ @@ -59,6 +59,9 @@ var v0_1_0_default []byte //go:embed v0.1.1.yaml var v0_1_1_default []byte +//go:embed v0.1.2.yaml +var v0_1_2_default []byte + // Map of context name -> content var ContextYamls = map[string][]byte{ "0.0.1": v0_0_1_default, @@ -72,6 +75,7 @@ var ContextYamls = map[string][]byte{ "0.0.9": v0_0_9_default, "0.1.0": v0_1_0_default, "0.1.1": v0_1_1_default, + "0.1.2": v0_1_2_default, } // Map of sequential migrations @@ -146,6 +150,13 @@ var MigrationChain = []migration.MigrationStep{ OldYAML: v0_1_0_default, NewYAML: v0_1_1_default, }, + { + From: "0.1.1", + To: "0.1.2", + Apply: contextMigrations.Migration_0_1_1_to_0_1_2, + OldYAML: v0_1_1_default, + NewYAML: v0_1_2_default, + }, } func MigrateContexts(logger iface.Logger) (int, error) { diff --git a/config/contexts/v0.1.2.yaml b/config/contexts/v0.1.2.yaml new file mode 100644 index 00000000..bb35585c --- /dev/null +++ b/config/contexts/v0.1.2.yaml @@ -0,0 +1,186 @@ +# Devnet context to be used for local deployments against Anvil chain +version: 0.1.2 +context: + # Name of the context + name: "devnet" + # Chains available to this context + chains: + l1: + chain_id: 31337 + rpc_url: "http://localhost:8545" + fork: + block: 9259079 + url: "" + block_time: 3 + l2: + chain_id: 31338 + rpc_url: "http://localhost:9545" + fork: + block: 31408197 + url: "" + block_time: 3 + # Devnet mnemonic for unlocked accounts + mnemonic: "test test test test test test test test test test test junk" + # Stake Root Transporter configuration + transporter: + schedule: "0 */2 * * *" + private_key: "0x5f8e6420b9cb0c940e3d3f8b99177980785906d16fb3571f70d7a05ecf5f2172" + bls_private_key: "0x5f8e6420b9cb0c940e3d3f8b99177980785906d16fb3571f70d7a05ecf5f2172" + active_stake_roots: [] + # All key material (BLS and ECDSA) within this file should be used for local testing ONLY + # ECDSA keys used are from Anvil's private key set + # BLS keystores are deterministically pre-generated and embedded. These are NOT derived from a secure seed + # Available private keys for deploying + deployer_private_key: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" # Anvil Private Key 0 + app_private_key: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" # Anvil Private Key 2 + # List of stakers and their delegations + stakers: + - address: "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f" + ecdsa_key: "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97" # Anvil 8 + deposits: + - strategy_address: "0x8b29d91e67b013e855EaFe0ad704aC4Ab086a574" + name: "stETH_Strategy" + deposit_amount: "5ETH" # depositIntoStrategy amount + operator: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" # Operator to delegate the stake via delegationManager.delegateTo() + - address: "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" + ecdsa_key: "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + deposits: + - strategy_address: "0x8b29d91e67b013e855EaFe0ad704aC4Ab086a574" + name: "stETH_Strategy" + deposit_amount: "5ETH" # depositIntoStrategy amount + operator: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" + # List of Operators and their private keys / stake details + operators: + - address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" + ecdsa_key: "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" + keystores: + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 0 + ecdsa_keystore_path: "keystores/operator1.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator1.bls.keystore.json" + bls_keystore_password: "testpass" + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 1 + ecdsa_keystore_path: "keystores/operator1.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator1.bls.keystore.json" + bls_keystore_password: "testpass" + allocations: + - strategy_address: "0x8b29d91e67b013e855EaFe0ad704aC4Ab086a574" + name: "stETH_Strategy" + # Only allocate if these operator set IDs exist in the deployed operator_sets + operator_set_allocations: + - operator_set: "0" + allocation_in_wads: "500000000000000000" # 5e17 i.e 50% of max allocation + - operator_set: "1" + allocation_in_wads: "500000000000000000" # 5e17 i.e 50% of max allocation + - address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" + ecdsa_key: "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a" # Anvil Private Key 4 + keystores: + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 0 + ecdsa_keystore_path: "keystores/operator2.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator2.bls.keystore.json" + bls_keystore_password: "testpass" + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 1 + ecdsa_keystore_path: "keystores/operator2.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator2.bls.keystore.json" + bls_keystore_password: "testpass" + allocations: + - strategy_address: "0x8b29d91e67b013e855EaFe0ad704aC4Ab086a574" + name: "stETH_Strategy" + # Only allocate if these operator set IDs exist in the deployed operator_sets + operator_set_allocations: + - operator_set: "0" + allocation_in_wads: "500000000000000000" # 5e17 i.e 50% of max allocation + - operator_set: "1" + allocation_in_wads: "500000000000000000" # 5e17 i.e 50% of max allocation + - address: "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc" + ecdsa_key: "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba" # Anvil Private Key 5 + keystores: + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 0 + ecdsa_keystore_path: "keystores/operator3.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator3.bls.keystore.json" + bls_keystore_password: "testpass" + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 1 + ecdsa_keystore_path: "keystores/operator3.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator3.bls.keystore.json" + bls_keystore_password: "testpass" + - address: "0x976EA74026E726554dB657fA54763abd0C3a0aa9" + ecdsa_key: "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e" # Anvil Private Key 6 + keystores: + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 0 + ecdsa_keystore_path: "keystores/operator4.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator4.bls.keystore.json" + bls_keystore_password: "testpass" + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 1 + ecdsa_keystore_path: "keystores/operator4.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator4.bls.keystore.json" + bls_keystore_password: "testpass" + - address: "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955" + ecdsa_key: "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356" # Anvil Private Key 7 + keystores: + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 0 + ecdsa_keystore_path: "keystores/operator5.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator5.bls.keystore.json" + bls_keystore_password: "testpass" + - avs: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + operatorSet: 1 + ecdsa_keystore_path: "keystores/operator5.ecdsa.keystore.json" + ecdsa_keystore_password: "testpass" + bls_keystore_path: "keystores/operator5.bls.keystore.json" + bls_keystore_password: "testpass" + # AVS configuration + avs: + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + avs_private_key: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" # Anvil Private Key 1 + metadata_url: "https://my-org.com/avs/metadata.json" + registrar_address: "0x0123456789abcdef0123456789ABCDEF01234567" + # Core EigenLayer contract addresses + eigenlayer: + l1: + allocation_manager: "0x42583067658071247ec8CE0A516A58f682002d07" + delegation_manager: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b" + strategy_manager: "0x2E3D6c0744b10eb0A4e6F679F71554a39Ec47a5D" + bn254_table_calculator: "0xa19E3B00cf4aC46B5e6dc0Bbb0Fb0c86D0D65603" + ecdsa_table_calculator: "0xaCB5DE6aa94a1908E6FA577C2ade65065333B450" + cross_chain_registry: "0x287381B1570d9048c4B4C7EC94d21dDb8Aa1352a" + key_registrar: "0xA4dB30D08d8bbcA00D40600bee9F029984dB162a" + release_manager: "0xd9Cb89F1993292dEC2F973934bC63B0f2A702776" + operator_table_updater: "0xB02A15c6Bd0882b35e9936A9579f35FB26E11476" + task_mailbox: "0xB99CC53e8db7018f557606C2a5B066527bF96b26" + permission_controller: "0x44632dfBdCb6D3E21EF613B0ca8A6A0c618F5a37" + l2: + bn254_certificate_verifier: "0xff58A373c18268F483C1F5cA03Cf885c0C43373a" + operator_table_updater: "0xB02A15c6Bd0882b35e9936A9579f35FB26E11476" + ecdsa_certificate_verifier: "0xb3Cd1A457dEa9A9A6F6406c6419B1c326670A96F" + task_mailbox: "0xB99CC53e8db7018f557606C2a5B066527bF96b26" + # L1 Contracts deployed on `devnet start` + deployed_l1_contracts: [] + # L2 Contracts deployed on `devnet start` + deployed_l2_contracts: [] + # Operator Sets registered on `devnet start` + operator_sets: [] + # Operators registered on `devnet start` + operator_registrations: [] + # Release artifact + artifact: + artifactId: "" + component: "" + digest: "" + registry: "" + version: "" diff --git a/go.mod b/go.mod index 495fbb89..066cc9cb 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,8 @@ require ( ) require ( + github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect + github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -81,6 +83,8 @@ require ( github.com/supranational/blst v0.3.15 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tyler-smith/go-bip32 v1.0.0 // indirect + github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/wealdtech/go-merkletree/v2 v2.6.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/go.sum b/go.sum index e483b7be..72157662 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,10 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U= github.com/Layr-Labs/crypto-libs v0.0.4 h1:FV/staDn/1CzYmmbP/o2+fPc9Z1NotPBP9dW/xisx94= github.com/Layr-Labs/crypto-libs v0.0.4/go.mod h1:PWjHsuxgk5MNopPr3QLhpP/RJerbjh98qCCSivnVPHE= github.com/Layr-Labs/eigenlayer-contracts v1.8.0-testnet-final.0.20250922221242-73644e201541 h1:QQp6CMOhOA2L9bsA1NTRYmTT0/mmxnD/7/WgD10XH54= @@ -42,6 +46,7 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= @@ -278,6 +283,7 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -290,6 +296,10 @@ github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8O github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE= +github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/wealdtech/go-merkletree/v2 v2.6.1 h1:EKrzJep7JXHk1bYQAHtEcBvScqW1xgI86aF5y6iPAm0= @@ -327,6 +337,7 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -410,6 +421,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= oras.land/oras-go/v2 v2.3.1 h1:lUC6q8RkeRReANEERLfH86iwGn55lbSWP20egdFHVec= oras.land/oras-go/v2 v2.3.1/go.mod h1:5AQXVEu1X/FKp1F9DMOb5ZItZBOa0y5dha0yCm4NR9c= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/pkg/commands/devnet.go b/pkg/commands/devnet.go index d09e0ff6..5a00e151 100644 --- a/pkg/commands/devnet.go +++ b/pkg/commands/devnet.go @@ -40,6 +40,11 @@ var DevnetCommand = &cli.Command{ Usage: "Specify a custom port for local devnet L2", Value: 9545, }, + &cli.BoolFlag{ + Name: "skip-forking", + Usage: "Skip forking L1 & L2 and deploy against provided RPCs", + Value: false, + }, &cli.BoolFlag{ Name: "skip-avs-run", Usage: "Skip starting offchain AVS components", diff --git a/pkg/commands/devnet_actions.go b/pkg/commands/devnet_actions.go index 9c8acb17..9de1197c 100644 --- a/pkg/commands/devnet_actions.go +++ b/pkg/commands/devnet_actions.go @@ -32,20 +32,12 @@ import ( ) func StartDevnetAction(cCtx *cli.Context) error { - // Check if docker is running, else try to start it - if err := common.EnsureDockerIsRunning(cCtx); err != nil { - - if errors.Is(err, context.Canceled) { - return err // propagate the cancellation directly - } - return cli.Exit(err.Error(), 1) - } - // Get logger logger := common.LoggerFromContext(cCtx) // Extract vars contextName := cCtx.String("context") + skipForking := cCtx.Bool("skip-forking") skipAvsRun := cCtx.Bool("skip-avs-run") skipDeployContracts := cCtx.Bool("skip-deploy-contracts") skipTransporter := cCtx.Bool("skip-transporter") @@ -81,6 +73,17 @@ func StartDevnetAction(cCtx *cli.Context) error { return fmt.Errorf("loading config and context failed: %w", err) } + // Check if docker is running, else try to start it + if skipForking == false { + if err := common.EnsureDockerIsRunning(cCtx); err != nil { + + if errors.Is(err, context.Canceled) { + return err // propagate the cancellation directly + } + return cli.Exit(err.Error(), 1) + } + } + // Prevent runs when context is not devnet if contextName != devnet.DEVNET_CONTEXT { return fmt.Errorf("devnet start failed: `devkit avs devnet start` only available on devnet - please run `devkit avs devnet start --context devnet`") @@ -113,138 +116,175 @@ func StartDevnetAction(cCtx *cli.Context) error { } } } - l1Port := cCtx.Int("l1-port") - l2Port := cCtx.Int("l2-port") - - if !devnet.IsPortAvailable(l2Port) { - return fmt.Errorf("❌ Port %d is already in use. Please choose a different port using --l2-port", l2Port) - } - - if !devnet.IsPortAvailable(l1Port) { - return fmt.Errorf("❌ Port %d is already in use. Please choose a different port using --l1-port", l1Port) - } - if !devnet.IsPortAvailable(l2Port) { - return fmt.Errorf("❌ L2 port %d is already in use. Please choose a different port using --port", l2Port) - } - - chainImage := devnet.GetDevnetChainImageOrDefault(config) - l1ChainArgs := devnet.GetL1DevnetChainArgsOrDefault(config) - l2ChainArgs := devnet.GetL2DevnetChainArgsOrDefault(config) // Start timer startTime := time.Now() - logger.Info("Starting L1 and L2 devnets...\n") + // Mnemonic to use for unlocked accounts in anvil and for funding + mnemonic := envCtx.Mnemonic + if mnemonic == "" { + mnemonic = devnet.DEFAULT_MNEMONIC + } - // Docker-compose for anvil devnet - composePath := devnet.WriteEmbeddedArtifacts() - l1ForkUrl, err := common.GetForkUrlDefault(contextName, config, common.L1) + // Get RPC url + l1RpcUrl, err := common.GetRPCUrlDefault(contextName, config, common.L1) if err != nil { - return fmt.Errorf("L1 fork URL error %w", err) + return fmt.Errorf("L1 RPC URL error %w", err) } - l2ForkUrl, err := common.GetForkUrlDefault(contextName, config, common.L2) + l2RpcUrl, err := common.GetRPCUrlDefault(contextName, config, common.L2) if err != nil { - return fmt.Errorf("L2 fork URL error: %w", err) + return fmt.Errorf("L2 RPC URL error %w", err) } - // Error if the l1ForkUrl has not been modified - if l1ForkUrl == "" { - return fmt.Errorf("l1 fork-url not set; set l1 fork-url in ./config/context/devnet.yaml or .env and consult README for guidance") - } - // Error if the l2ForkUrl has not been modified - if l2ForkUrl == "" { - return fmt.Errorf("l2 fork-url not set; set l2 fork-url in ./config/context/devnet.yaml or .env and consult README for guidance") - } + // Describe where the deployment is taking place + l1ChainDescription := fmt.Sprintf("against RPC %s", l1RpcUrl) + l2ChainDescription := fmt.Sprintf("against RPC %s", l2RpcUrl) - // Ensure fork URL uses appropriate Docker host for container environments - l1DockerForkUrl := devnet.EnsureDockerHost(l1ForkUrl) - l2DockerForkUrl := devnet.EnsureDockerHost(l2ForkUrl) - // Get the l1 block_time from env/config - l1BlockTime, err := devnet.GetDevnetBlockTimeOrDefault(config, common.L1) - if err != nil { - l1BlockTime = 12 - } + // Start anvil containers + if skipForking == false { + l1Port := cCtx.Int("l1-port") + l2Port := cCtx.Int("l2-port") - // Get the l2 block_time from env/config - l2BlockTime, err := devnet.GetDevnetBlockTimeOrDefault(config, common.L2) - if err != nil { - l2BlockTime = 12 - } + if !devnet.IsPortAvailable(l2Port) { + return fmt.Errorf("❌ Port %d is already in use. Please choose a different port using --l2-port", l2Port) + } - // Get the l1 chain_id from env/config - l1ChainId, err := devnet.GetDevnetChainIdOrDefault(config, common.L1, logger) - if err != nil { - l1ChainId = devnet.DEFAULT_L1_ANVIL_CHAINID - } + if !devnet.IsPortAvailable(l1Port) { + return fmt.Errorf("❌ Port %d is already in use. Please choose a different port using --l1-port", l1Port) + } + if !devnet.IsPortAvailable(l2Port) { + return fmt.Errorf("❌ L2 port %d is already in use. Please choose a different port using --port", l2Port) + } - // Get the l2 chain_id from env/config - l2ChainId, err := devnet.GetDevnetChainIdOrDefault(config, common.L2, logger) - if err != nil { - l2ChainId = devnet.DEFAULT_L2_ANVIL_CHAINID - } - - // Append config defined details to chainArgs for l1 - l1ChainArgs = fmt.Sprintf("%s --chain-id %d", l1ChainArgs, l1ChainId) - l1ChainArgs = fmt.Sprintf("%s --block-time %d", l1ChainArgs, l1BlockTime) - - // Append config defined details to chainArgs for l2 - l2ChainArgs = fmt.Sprintf("%s --chain-id %d", l2ChainArgs, l2ChainId) - l2ChainArgs = fmt.Sprintf("%s --block-time %d", l2ChainArgs, l2BlockTime) - - // Run docker compose up for anvil devnet - cmd := exec.CommandContext(cCtx.Context, "docker", "compose", "-p", config.Config.Project.Name, "-f", composePath, "up", "-d") - - l1ContainerName := fmt.Sprintf("devkit-devnet-l1-%s", config.Config.Project.Name) - l2ContainerName := fmt.Sprintf("devkit-devnet-l2-%s", config.Config.Project.Name) - l1ChainConfig, found := envCtx.Chains[common.L1] - if !found { - return fmt.Errorf("failed to find a chain with name: l1 in devnet.yaml") - } - l2ChainConfig, found := envCtx.Chains[common.L2] - if !found { - return fmt.Errorf("failed to find a chain with name: l2 in devnet.yaml") - } - - cmd.Env = append(os.Environ(), - "FOUNDRY_IMAGE="+chainImage, - "L1_ANVIL_ARGS="+l1ChainArgs, - "L2_ANVIL_ARGS="+l2ChainArgs, - fmt.Sprintf("L1_DEVNET_PORT=%d", l1Port), - fmt.Sprintf("L2_DEVNET_PORT=%d", l2Port), - "L1_FORK_RPC_URL="+l1DockerForkUrl, - "L2_FORK_RPC_URL="+l2DockerForkUrl, - fmt.Sprintf("L1_FORK_BLOCK_NUMBER=%d", l1ChainConfig.Fork.Block), - fmt.Sprintf("L2_FORK_BLOCK_NUMBER=%d", l2ChainConfig.Fork.Block), - "L1_AVS_CONTAINER_NAME="+l1ContainerName, - "L2_AVS_CONTAINER_NAME="+l2ContainerName, - ) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("❌ Failed to start devnet: %w", err) + chainImage := devnet.GetDevnetChainImageOrDefault(config) + l1ChainArgs := devnet.GetL1DevnetChainArgsOrDefault(config) + l2ChainArgs := devnet.GetL2DevnetChainArgsOrDefault(config) + + logger.Info("Starting L1 and L2 devnets...\n") + + // Docker-compose for anvil devnet + composePath := devnet.WriteEmbeddedArtifacts() + + // Get fork url + l1ForkUrl, err := common.GetForkUrlDefault(contextName, config, common.L1) + if err != nil { + return fmt.Errorf("L1 fork URL error %w", err) + } + l2ForkUrl, err := common.GetForkUrlDefault(contextName, config, common.L2) + if err != nil { + return fmt.Errorf("L2 fork URL error: %w", err) + } + + // Error if the l1ForkUrl has not been modified + if l1ForkUrl == "" { + return fmt.Errorf("l1 fork-url not set; set l1 fork-url in ./config/context/devnet.yaml or .env and consult README for guidance") + } + // Error if the l2ForkUrl has not been modified + if l2ForkUrl == "" { + return fmt.Errorf("l2 fork-url not set; set l2 fork-url in ./config/context/devnet.yaml or .env and consult README for guidance") + } + + // Ensure fork URL uses appropriate Docker host for container environments + l1DockerForkUrl := devnet.EnsureDockerHost(l1ForkUrl) + l2DockerForkUrl := devnet.EnsureDockerHost(l2ForkUrl) + + // Get the l1 block_time from env/config + l1BlockTime, err := devnet.GetDevnetBlockTimeOrDefault(config, common.L1) + if err != nil { + l1BlockTime = 12 + } + + // Get the l2 block_time from env/config + l2BlockTime, err := devnet.GetDevnetBlockTimeOrDefault(config, common.L2) + if err != nil { + l2BlockTime = 12 + } + + // Get the l1 chain_id from env/config + l1ChainId, err := devnet.GetDevnetChainIdOrDefault(config, common.L1, logger) + if err != nil { + l1ChainId = devnet.DEFAULT_L1_ANVIL_CHAINID + } + + // Get the l2 chain_id from env/config + l2ChainId, err := devnet.GetDevnetChainIdOrDefault(config, common.L2, logger) + if err != nil { + l2ChainId = devnet.DEFAULT_L2_ANVIL_CHAINID + } + + // Append config defined details to chainArgs for l1 + l1ChainArgs = fmt.Sprintf("%s --chain-id %d", l1ChainArgs, l1ChainId) + l1ChainArgs = fmt.Sprintf("%s --block-time %d", l1ChainArgs, l1BlockTime) + l1ChainArgs = fmt.Sprintf("%s --mnemonic \"%s\"", l1ChainArgs, mnemonic) + + // Append config defined details to chainArgs for l2 + l2ChainArgs = fmt.Sprintf("%s --chain-id %d", l2ChainArgs, l2ChainId) + l2ChainArgs = fmt.Sprintf("%s --block-time %d", l2ChainArgs, l2BlockTime) + l2ChainArgs = fmt.Sprintf("%s --mnemonic \"%s\"", l2ChainArgs, mnemonic) + + // Run docker compose up for anvil devnet + cmd := exec.CommandContext(cCtx.Context, "docker", "compose", "-p", config.Config.Project.Name, "-f", composePath, "up", "-d") + + l1ContainerName := fmt.Sprintf("devkit-devnet-l1-%s", config.Config.Project.Name) + l2ContainerName := fmt.Sprintf("devkit-devnet-l2-%s", config.Config.Project.Name) + l1ChainConfig, found := envCtx.Chains[common.L1] + if !found { + return fmt.Errorf("failed to find a chain with name: l1 in devnet.yaml") + } + l2ChainConfig, found := envCtx.Chains[common.L2] + if !found { + return fmt.Errorf("failed to find a chain with name: l2 in devnet.yaml") + } + + cmd.Env = append(os.Environ(), + "FOUNDRY_IMAGE="+chainImage, + "L1_ANVIL_ARGS="+l1ChainArgs, + "L2_ANVIL_ARGS="+l2ChainArgs, + fmt.Sprintf("L1_DEVNET_PORT=%d", l1Port), + fmt.Sprintf("L2_DEVNET_PORT=%d", l2Port), + "L1_FORK_RPC_URL="+l1DockerForkUrl, + "L2_FORK_RPC_URL="+l2DockerForkUrl, + fmt.Sprintf("L1_FORK_BLOCK_NUMBER=%d", l1ChainConfig.Fork.Block), + fmt.Sprintf("L2_FORK_BLOCK_NUMBER=%d", l2ChainConfig.Fork.Block), + "L1_AVS_CONTAINER_NAME="+l1ContainerName, + "L2_AVS_CONTAINER_NAME="+l2ContainerName, + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("❌ Failed to start devnet: %w", err) + } + + // Construct RPC url to pass to scripts for l1 and l2 + l1RpcUrl = devnet.GetRPCURL(l1Port) + l2RpcUrl = devnet.GetRPCURL(l2Port) + + // Describe where the deployment is taking place + l1ChainDescription = fmt.Sprintf("on port %d", l1Port) + l2ChainDescription = fmt.Sprintf("on port %d", l2Port) + + // Wait for containers to be ready and funded + logger.Info("Waiting for devnet to be ready...") } // On cancel, stop the containers if we're not skipping deployContracts/avsRun and we're not persisting if !skipDeployContracts && !skipAvsRun && !persist { defer func() { - logger.Info("Stopping containers") - // Use background context to avoid cancellation issues during cleanup - bgCtx := context.Background() + if skipForking == false { + logger.Info("Stopping containers") + // Use background context to avoid cancellation issues during cleanup + bgCtx := context.Background() - l1Container := fmt.Sprintf("devkit-devnet-l1-%s", config.Config.Project.Name) - l2Container := fmt.Sprintf("devkit-devnet-l2-%s", config.Config.Project.Name) + l1Container := fmt.Sprintf("devkit-devnet-l1-%s", config.Config.Project.Name) + l2Container := fmt.Sprintf("devkit-devnet-l2-%s", config.Config.Project.Name) - logger.Info("Stopping individual containers: %s, %s", l1Container, l2Container) - devnet.StopAndRemoveContainer(&cli.Context{Context: bgCtx}, l1Container) - devnet.StopAndRemoveContainer(&cli.Context{Context: bgCtx}, l2Container) + logger.Info("Stopping individual containers: %s, %s", l1Container, l2Container) + devnet.StopAndRemoveContainer(&cli.Context{Context: bgCtx}, l1Container) + devnet.StopAndRemoveContainer(&cli.Context{Context: bgCtx}, l2Container) + } }() } - // Construct RPC url to pass to scripts for l1 and l2 - l1RpcUrl := devnet.GetRPCURL(l1Port) - l2RpcUrl := devnet.GetRPCURL(l2Port) - logger.Info("Waiting for devnet to be ready...") - // Get chains node chainsNode := common.GetChildByKey(contextNode, "chains") if chainsNode == nil { @@ -278,15 +318,15 @@ func StartDevnetAction(cCtx *cli.Context) error { time.Sleep(4 * time.Second) // Fund the wallets defined in config on L1 - logger.Info("Funding wallets on L1...") - err = devnet.FundWalletsDevnet(config, l1RpcUrl) + logger.Info("Funding wallets on L1 (%s)...", l1RpcUrl) + err = devnet.FundWalletsDevnet(config, l1RpcUrl, mnemonic) if err != nil { return fmt.Errorf("funding L1 devnet wallets failed - please restart devnet and try again: %w", err) } // Fund the wallets defined in config on L2 - logger.Info("Funding wallets on L2...") - err = devnet.FundWalletsDevnet(config, l2RpcUrl) + logger.Info("Funding wallets on L2 (%s)...", l2RpcUrl) + err = devnet.FundWalletsDevnet(config, l2RpcUrl, mnemonic) if err != nil { return fmt.Errorf("failed L2 devnet wallets failed - please restart devnet and try again: %w", err) } @@ -304,7 +344,8 @@ func StartDevnetAction(cCtx *cli.Context) error { } if len(tokenAddresses) > 0 { - err = devnet.FundStakersWithStrategyTokens(config, l1RpcUrl, tokenAddresses) + // Use DEAD address to mint strategy tokens to avoid minting to BURN + err = devnet.FundStakersWithStrategyTokens(config, l1RpcUrl, tokenAddresses, mnemonic) if err != nil { logger.Warn("Failed to fund stakers with strategy tokens: %v", err) logger.Info("Continuing with devnet startup...") @@ -320,20 +361,23 @@ func StartDevnetAction(cCtx *cli.Context) error { // Sleep for 1 second to make sure wallets are funded time.Sleep(1 * time.Second) - logger.Info("\nL1 devnet started successfully on port %d", l1Port) - logger.Info("L2 devnet started successfully on port %d", l2Port) + logger.Info("\nL1 devnet started successfully %s", l1ChainDescription) + logger.Info("L2 devnet started successfully %s", l2ChainDescription) logger.Info("Total startup time: %s", elapsed) - if err := WhitelistChainIdInCrossRegistryAction(cCtx, logger); err != nil { + if err := WhitelistChainIdInCrossRegistryAction(cCtx, logger, mnemonic); err != nil { return fmt.Errorf("whitelisting chain id in cross registry failed - please restart devnet and try again: %w", err) } // Deploy the contracts after starting devnet unless skipped if !skipDeployContracts { - // Check if docker is running, else try to start it - err := common.EnsureDockerIsRunning(cCtx) - if err != nil { - return cli.Exit(err.Error(), 1) + // We only need docker if we're forking locally + if skipForking == false { + // Check if docker is running, else try to start it + err := common.EnsureDockerIsRunning(cCtx) + if err != nil { + return cli.Exit(err.Error(), 1) + } } // Call deploy L1 action within devnet context @@ -1225,10 +1269,15 @@ func SetAllocationDelayAction(cCtx *cli.Context, logger iface.Logger) error { } defer client.Close() - // Instead of mining blocks(because it's infeasible for 126000 blocks(for mainnet) or 30 on sepolia), use anvil_setStorageAt to bypass ALLOCATION_CONFIGURATION_DELAY + method, err := devnet.DetectSetStorageMethod(client.Client()) + if err != nil { + return fmt.Errorf("failed to detect setStorageAt method: %w", err) + } + + // Instead of mining blocks(because it's infeasible for 126000 blocks(for mainnet) or 30 on sepolia), use *_setStorageAt to bypass ALLOCATION_CONFIGURATION_DELAY // We need to manipulate the storage that tracks when allocation delays were set for each operator by modifying // the effectBlock field in the AllocationDelayInfo struct. - logger.Info("Bypassing allocation configuration delay using anvil_setStorageAt...") + logger.Info("Bypassing allocation configuration delay using %s...", method) allocationManagerAddr, _, _, _, _, _, _, _ := common.GetEigenLayerAddresses(contextName, cfg) currentBlock, err := client.BlockNumber(cCtx.Context) @@ -1281,10 +1330,10 @@ func SetAllocationDelayAction(cCtx *cli.Context, logger iface.Logger) error { binary.BigEndian.PutUint32(structValue[offset:], effectBlock) var setStorageResult interface{} - err = rpcClient.Call(&setStorageResult, "anvil_setStorageAt", + err = rpcClient.Call(&setStorageResult, method, allocationManagerAddr, storageKey.Hex(), - hex.EncodeToString(structValue)) + "0x"+hex.EncodeToString(structValue)) if err != nil { logger.Warn("Failed to manipulate AllocationDelayInfo storage for operator %s: %v", op.Address, err) } else { @@ -1297,7 +1346,7 @@ func SetAllocationDelayAction(cCtx *cli.Context, logger iface.Logger) error { return nil } -func WhitelistChainIdInCrossRegistryAction(cCtx *cli.Context, logger iface.Logger) error { +func WhitelistChainIdInCrossRegistryAction(cCtx *cli.Context, logger iface.Logger, mnemonic string) error { // Extract vars contextName := cCtx.String("context") diff --git a/pkg/commands/transporter.go b/pkg/commands/transporter.go index a98943a5..c8938d5a 100644 --- a/pkg/commands/transporter.go +++ b/pkg/commands/transporter.go @@ -499,22 +499,25 @@ func Transport(cCtx *cli.Context, initialRun bool) error { } for _, opset := range opsets { - err = stakeTransport.SignAndTransportAvsStakeTable( - cCtx.Context, - referenceTimestamp, - l1Block.NumberU64(), - opset, - root, - tree, - dist, - ignoreChainIds, - ) - if err != nil { - return fmt.Errorf("failed to sign and transport AVS stake table for opset %v: %v", opset, err) - } + // Only transport the configured AVSs operatorSets + if strings.ToLower(opset.Avs.Hex()) == strings.ToLower(envCtx.Avs.Address) { + err = stakeTransport.SignAndTransportAvsStakeTable( + cCtx.Context, + referenceTimestamp, + l1Block.NumberU64(), + opset, + root, + tree, + dist, + ignoreChainIds, + ) + if err != nil { + return fmt.Errorf("failed to sign and transport AVS stake table for opset %v: %v", opset, err) + } - // log success - logger.Info("Successfully signed and transported AVS stake table for opset %v", opset) + // log success + logger.Info("Successfully signed and transported AVS stake table for opset %v", opset) + } } return nil diff --git a/pkg/common/config.go b/pkg/common/config.go index 132e1a99..73dbd1f6 100644 --- a/pkg/common/config.go +++ b/pkg/common/config.go @@ -196,6 +196,7 @@ type ArtifactConfig struct { type ChainContextConfig struct { Name string `json:"name" yaml:"name"` Chains map[string]ChainConfig `json:"chains" yaml:"chains"` + Mnemonic string `json:"mnemonic,omitempty" yaml:"mnemonic,omitempty"` Transporter Transporter `json:"transporter,omitempty" yaml:"transporter,omitempty"` DeployerPrivateKey string `json:"deployer_private_key" yaml:"deployer_private_key"` AppDeployerPrivateKey string `json:"app_private_key" yaml:"app_private_key"` diff --git a/pkg/common/contract_caller.go b/pkg/common/contract_caller.go index 5d705264..9c1893e9 100644 --- a/pkg/common/contract_caller.go +++ b/pkg/common/contract_caller.go @@ -641,28 +641,6 @@ func (cc *ContractCaller) WhitelistChainIdInCrossRegistry(ctx context.Context, o // Get RPC client from ethclient rpcClient := cc.ethclient.Client() - // Check if owner already has sufficient balance - balance, err := cc.ethclient.BalanceAt(ctx, ownerCrossChainRegistry, nil) - if err != nil { - return fmt.Errorf("failed to get owner balance: %w", err) - } - - // Only fund if balance is less than 0.1 ETH - minBalance := big.NewInt(100000000000000000) // 0.1 ETH in wei - if balance.Cmp(minBalance) < 0 { - cc.logger.Info("Funding cross chain registry owner with 1 ETH") - - // Use anvil_setBalance RPC method - err = rpcClient.Call(nil, "anvil_setBalance", ownerCrossChainRegistry.Hex(), "0x8AC7230489E80000") // 10 ETH in hex - if err != nil { - return fmt.Errorf("failed to set owner balance: %w", err) - } - - cc.logger.Info("Successfully set owner balance to 10 ETH") - } else { - cc.logger.Info("Owner already has sufficient balance: %s wei", balance.String()) - } - if err := ImpersonateAccount(rpcClient, ownerCrossChainRegistry); err != nil { return fmt.Errorf("failed to impersonate account: %w", err) } @@ -696,7 +674,7 @@ func (cc *ContractCaller) WhitelistChainIdInCrossRegistry(ctx context.Context, o err = rpcClient.Call(&txHash, "eth_sendTransaction", map[string]interface{}{ "from": ownerCrossChainRegistry.Hex(), "to": cc.crossChainRegistryAddr.Hex(), - "gas": "0x30d40", // 200000 in hex + "gas": "0x493e0", // 300000 in hex "gasPrice": fmt.Sprintf("0x%x", gasPrice), "value": "0x0", "data": fmt.Sprintf("0x%x", addChainIDsToWhitelistData), diff --git a/pkg/common/devnet/constants.go b/pkg/common/devnet/constants.go index 9e4dabe0..da9fe1a1 100644 --- a/pkg/common/devnet/constants.go +++ b/pkg/common/devnet/constants.go @@ -1,14 +1,14 @@ package devnet +const DEVNET_CONTEXT = "devnet" + // Foundry Image Date : 21 April 2025 const FOUNDRY_IMAGE = "ghcr.io/foundry-rs/foundry:stable" const L1_CHAIN_ARGS = "--gas-limit 140000000 --base-fee 0 --gas-price 1000000 --no-rate-limit" const L2_CHAIN_ARGS = "--gas-limit 140000000 --base-fee 0 --gas-price 1000000 --no-rate-limit" -const FUND_VALUE = "1000000000000000000" -const DEVNET_CONTEXT = "devnet" -const ANVIL_1_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" -const ANVIL_2_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +// Default funding amount for Operators and funded addresses +const FUND_VALUE = "10000000000000000000" // Ref https://github.com/Layr-Labs/eigenlayer-contracts/blob/c08c9e849c27910f36f3ab746f3663a18838067f/src/contracts/core/AllocationManagerStorage.sol#L63 const ALLOCATION_DELAY_INFO_SLOT = 155 @@ -33,6 +33,8 @@ const L2_CONTAINER_NAME_PREFIX = "devkit-devnet-l2-" const L1_CONTAINER_TYPE = "l1" const L2_CONTAINER_TYPE = "l2" +const DEFAULT_MNEMONIC = "test test test test test test test test test test test junk" + const DEFAULT_L1_ANVIL_CHAINID = 31337 const DEFAULT_L2_ANVIL_CHAINID = 31338 diff --git a/pkg/common/devnet/funding.go b/pkg/common/devnet/funding.go index f97d6947..435f1721 100644 --- a/pkg/common/devnet/funding.go +++ b/pkg/common/devnet/funding.go @@ -37,17 +37,17 @@ var DefaultTokenHolders = map[common.Address]TokenFunding{ common.HexToAddress(ST_ETH_TOKEN_ADDRESS): { // stETH token address TokenName: "stETH", HolderAddress: common.HexToAddress("0xC8088abD2FdaF4819230EB0FdA2D9766FDF9F409"), // Large stETH holder - Amount: new(big.Int).Mul(big.NewInt(STRATEGY_TOKEN_FUNDING_AMOUNT_BY_LARGE_HOLDER_IN_ETH), big.NewInt(1e18)), // 1000 tokens + Amount: new(big.Int).Mul(big.NewInt(STRATEGY_TOKEN_FUNDING_AMOUNT_BY_LARGE_HOLDER_IN_ETH), big.NewInt(1e17)), // .1 tokens }, common.HexToAddress(B_EIGEN_TOKEN_ADDRESS): { // bEIGEN token address TokenName: "bEIGEN", HolderAddress: common.HexToAddress("0x5f8C207382426D3f7F248E6321Cf93B34e66d6b9"), // Large EIGEN holder that calls unwrap() to get bEIGEN - Amount: new(big.Int).Mul(big.NewInt(STRATEGY_TOKEN_FUNDING_AMOUNT_BY_LARGE_HOLDER_IN_ETH), big.NewInt(1e18)), // 1000 tokens + Amount: new(big.Int).Mul(big.NewInt(STRATEGY_TOKEN_FUNDING_AMOUNT_BY_LARGE_HOLDER_IN_ETH), big.NewInt(1e17)), // .1 tokens }, } // FundStakerWithTokens funds staker with strategy tokens using impersonation -func FundStakerWithTokens(ctx context.Context, ethClient *ethclient.Client, rpcClient *rpc.Client, stakerAddress common.Address, tokenFunding TokenFunding, tokenAddress common.Address, rpcURL string) error { +func FundStakerWithTokens(ctx context.Context, ethClient *ethclient.Client, rpcClient *rpc.Client, stakerAddress common.Address, tokenFunding TokenFunding, tokenAddress common.Address, mnemonic string, rpcURL string) error { if tokenFunding.TokenName == "bEIGEN" { // For bEIGEN, we need to call unwrap() on the EIGEN contract first // to convert EIGEN tokens to bEIGEN tokens @@ -83,7 +83,7 @@ func FundStakerWithTokens(ctx context.Context, ethClient *ethclient.Client, rpcC // if holder balance < 0.1 ether, fund it fundValue, _ := strconv.ParseInt(FUND_VALUE, 10, 64) if balance.Cmp(big.NewInt(fundValue)) < 0 { - err = fundIfNeeded(ethClient, tokenFunding.HolderAddress, ANVIL_2_KEY) + err = FundIfNeeded(ethClient, tokenFunding.HolderAddress, mnemonic) if err != nil { return fmt.Errorf("failed to fund holder address: %w", err) } @@ -119,18 +119,14 @@ func FundStakerWithTokens(ctx context.Context, ethClient *ethclient.Client, rpcC log.Printf("⚠️ Failed to stop impersonating after unwrap %s: %v", tokenFunding.HolderAddress.Hex(), err) } } else if tokenFunding.TokenName == "stETH" { - // Get config - anvil1Key := ANVIL_2_KEY - anvil1Key = strings.TrimPrefix(anvil1Key, "0x") - privateKey, err := crypto.HexToECDSA(anvil1Key) + privateKey, err := GetPrivateKeyFromMnemonic(mnemonic, "", 1) if err != nil { - return fmt.Errorf("failed to parse private key: %w", err) + log.Fatalf("derive error: %v", err) } - - anvil1Address := crypto.PubkeyToAddress(privateKey.PublicKey) + fromAddress := crypto.PubkeyToAddress(privateKey.PublicKey) // Start impersonating the token holder - if err := devkitcommon.ImpersonateAccount(rpcClient, anvil1Address); err != nil { + if err := devkitcommon.ImpersonateAccount(rpcClient, fromAddress); err != nil { return fmt.Errorf("failed to impersonate token holder: %w", err) } @@ -155,9 +151,9 @@ func FundStakerWithTokens(ctx context.Context, ethClient *ethclient.Client, rpcC // Send submit transaction from impersonated account using RPC var submitTxHash common.Hash err = rpcClient.Call(&submitTxHash, "eth_sendTransaction", map[string]interface{}{ - "from": anvil1Address.Hex(), + "from": fromAddress.Hex(), "to": ST_ETH_TOKEN_ADDRESS, - "gas": "0x30d40", // 200000 in hex + "gas": "0x493e0", // 300000 in hex "gasPrice": fmt.Sprintf("0x%x", gasPrice), "value": fmt.Sprintf("0x%x", tokenFunding.Amount), "data": fmt.Sprintf("0x%x", submitData), @@ -187,7 +183,7 @@ func FundStakerWithTokens(ctx context.Context, ethClient *ethclient.Client, rpcC // Send transfer transaction from impersonated account using RPC var transferTxHash common.Hash err = rpcClient.Call(&transferTxHash, "eth_sendTransaction", map[string]interface{}{ - "from": anvil1Address.Hex(), + "from": fromAddress.Hex(), "to": ST_ETH_TOKEN_ADDRESS, "gas": "0x30d40", // 200000 in hex "gasPrice": fmt.Sprintf("0x%x", gasPrice), @@ -210,8 +206,8 @@ func FundStakerWithTokens(ctx context.Context, ethClient *ethclient.Client, rpcC } // Stop impersonating for transfer - if err := devkitcommon.StopImpersonatingAccount(rpcClient, anvil1Address); err != nil { - log.Printf("⚠️ Failed to stop impersonating after transfer %s: %v", anvil1Address.Hex(), err) + if err := devkitcommon.StopImpersonatingAccount(rpcClient, fromAddress); err != nil { + log.Printf("⚠️ Failed to stop impersonating after transfer %s: %v", fromAddress.Hex(), err) } } @@ -219,7 +215,7 @@ func FundStakerWithTokens(ctx context.Context, ethClient *ethclient.Client, rpcC } // FundStakersWithStrategyTokens funds all stakers with the specified strategy tokens -func FundStakersWithStrategyTokens(cfg *devkitcommon.ConfigWithContextConfig, rpcURL string, tokenAddresses []string) error { +func FundStakersWithStrategyTokens(cfg *devkitcommon.ConfigWithContextConfig, rpcURL string, tokenAddresses []string, mnemonic string) error { if os.Getenv("SKIP_TOKEN_FUNDING") == "true" { log.Println("🔧 Skipping token funding (test mode)") return nil @@ -253,7 +249,7 @@ func FundStakersWithStrategyTokens(cfg *devkitcommon.ConfigWithContextConfig, rp continue } - err := FundStakerWithTokens(ctx, ethClient, rpcClient, stakerAddr, tokenFunding, tokenAddress, rpcURL) + err := FundStakerWithTokens(ctx, ethClient, rpcClient, stakerAddr, tokenFunding, tokenAddress, mnemonic, rpcURL) if err != nil { log.Printf("❌ Failed to fund %s with %s (%s): %v", stakerAddr.Hex(), tokenFunding.TokenName, tokenAddressStr, err) continue @@ -289,7 +285,7 @@ func waitForTransaction(ctx context.Context, client *ethclient.Client, txHash co // FundWallets sends ETH to a list of addresses // Only funds wallets with balance < 0.3 ether. -func FundWalletsDevnet(cfg *devkitcommon.ConfigWithContextConfig, rpcURL string) error { +func FundWalletsDevnet(cfg *devkitcommon.ConfigWithContextConfig, rpcURL string, mnemonic string) error { if os.Getenv("SKIP_DEVNET_FUNDING") == "true" { log.Println("🔧 Skipping devnet wallet funding (test mode)") return nil @@ -331,19 +327,44 @@ func FundWalletsDevnet(cfg *devkitcommon.ConfigWithContextConfig, rpcURL string) } else { log.Fatalf("no ECDSA key configuration found for operator %s", operator.Address) } - err = fundIfNeeded(ethClient, crypto.PubkeyToAddress(privateKey.PublicKey), ANVIL_2_KEY) + err = FundIfNeeded(ethClient, crypto.PubkeyToAddress(privateKey.PublicKey), mnemonic) if err != nil { return err } } + // Fund deployer + deployerPrivateKey := cfg.Context[DEVNET_CONTEXT].DeployerPrivateKey + deployedECDSAKey, err := crypto.HexToECDSA(strings.TrimPrefix(deployerPrivateKey, "0x")) + if err != nil { + log.Fatalf("invalid private key %q: %v", deployerPrivateKey, err) + } + err = FundIfNeeded(ethClient, crypto.PubkeyToAddress(deployedECDSAKey.PublicKey), mnemonic) + if err != nil { + return err + } + + // Fund AVS + avsAddress := common.HexToAddress(cfg.Context[DEVNET_CONTEXT].Avs.Address) + err = FundIfNeeded(ethClient, avsAddress, mnemonic) + if err != nil { + return err + } + + // Fund crossChainRegistry owner + ownerCrossChainRegistry := common.HexToAddress(devkitcommon.CrossChainRegistryOwnerAddress) + err = FundIfNeeded(ethClient, ownerCrossChainRegistry, mnemonic) + if err != nil { + return err + } + // Fund transporter - privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(cfg.Context[DEVNET_CONTEXT].Transporter.PrivateKey, "0x")) + transporterPrivateKey := cfg.Context[DEVNET_CONTEXT].Transporter.PrivateKey + transporterECDSAKey, err := crypto.HexToECDSA(strings.TrimPrefix(transporterPrivateKey, "0x")) if err != nil { return fmt.Errorf("failed to parse private key: %w", err) } - - err = fundIfNeeded(ethClient, crypto.PubkeyToAddress(privateKey.PublicKey), ANVIL_2_KEY) + err = FundIfNeeded(ethClient, crypto.PubkeyToAddress(transporterECDSAKey.PublicKey), mnemonic) if err != nil { return err } @@ -351,7 +372,8 @@ func FundWalletsDevnet(cfg *devkitcommon.ConfigWithContextConfig, rpcURL string) return nil } -func fundIfNeeded(ethClient *ethclient.Client, to common.Address, fromKey string) error { +func FundIfNeeded(ethClient *ethclient.Client, to common.Address, mnemonic string) error { + // Check balance of recipient balance, err := ethClient.BalanceAt(context.Background(), to, nil) if err != nil { log.Printf(" Please check if your L1 and L2 fork rpc url is up") @@ -365,15 +387,13 @@ func fundIfNeeded(ethClient *ethclient.Client, to common.Address, fromKey string return nil } - value, _ := new(big.Int).SetString(FUND_VALUE, 10) // 1 ETH in wei + value, _ := new(big.Int).SetString(FUND_VALUE, 10) // 10 ETH in wei gasPrice, err := ethClient.SuggestGasPrice(context.Background()) if err != nil { return fmt.Errorf("failed to get gas price: %w", err) } - // Get the nonce for the sender - fromKey = strings.TrimPrefix(fromKey, "0x") - privateKey, err := crypto.HexToECDSA(fromKey) + privateKey, err := GetPrivateKeyFromMnemonic(mnemonic, "", 1) if err != nil { return fmt.Errorf("failed to parse private key: %w", err) } @@ -385,16 +405,16 @@ func fundIfNeeded(ethClient *ethclient.Client, to common.Address, fromKey string return fmt.Errorf("failed to get sender balance: %w", err) } - // Calculate total cost (value + gas) - gasLimit := uint64(21000) + // Calculate total cost (value + gas (large gas limit here to allow for fallbacks when contract)) + gasLimit := uint64(30000) totalCost := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasLimit))) totalCost.Add(totalCost, value) if senderBalance.Cmp(totalCost) < 0 { - return fmt.Errorf("funder has insufficient balance: has %s wei, needs %s wei", senderBalance.String(), totalCost.String()) + return fmt.Errorf("funder (%s) has insufficient balance: has %s wei, needs %s wei", fromAddress, senderBalance.String(), totalCost.String()) } - nonce, err := ethClient.PendingNonceAt(context.Background(), fromAddress) + nonce, err := ethClient.NonceAt(context.Background(), fromAddress, nil) if err != nil { return fmt.Errorf("failed to get nonce: %w", err) } @@ -426,7 +446,7 @@ func fundIfNeeded(ethClient *ethclient.Client, to common.Address, fromKey string return fmt.Errorf("failed to send transaction: %w", err) } - log.Printf("Transaction sent, waiting for confirmation...") + log.Printf("Transaction sent at nonce %d from %s with val %d (tx: %s), waiting for confirmation...", nonce, fromAddress, value, signedTx.Hash().Hex()) // Wait for transaction to be mined using bind.WaitMined receipt, err := bind.WaitMined(context.Background(), ethClient, signedTx) @@ -438,6 +458,20 @@ func fundIfNeeded(ethClient *ethclient.Client, to common.Address, fromKey string return fmt.Errorf("transaction failed") } + minedTx, _, err := ethClient.TransactionByHash(context.Background(), signedTx.Hash()) + if err != nil { + return err + } + if minedTx.To() == nil || *minedTx.To() != to { + return fmt.Errorf("unexpected tx.to: want %s got %v", to.Hex(), minedTx.To()) + } + if minedTx.Value().Cmp(value) != 0 { + return fmt.Errorf("unexpected tx value: want %s got %s", value, minedTx.Value()) + } + + newBal, _ := ethClient.BalanceAt(context.Background(), to, nil) + log.Printf("recipient balance after: %s wei", newBal.String()) + log.Printf("✅ Funded %s (tx: %s)", to, signedTx.Hash().Hex()) return nil } diff --git a/pkg/common/devnet/getters.go b/pkg/common/devnet/getters.go index 92600616..635f48ba 100644 --- a/pkg/common/devnet/getters.go +++ b/pkg/common/devnet/getters.go @@ -1,12 +1,17 @@ package devnet import ( + "crypto/ecdsa" "fmt" "os" "strconv" + "strings" "github.com/Layr-Labs/devkit-cli/pkg/common" "github.com/Layr-Labs/devkit-cli/pkg/common/iface" + "github.com/ethereum/go-ethereum/crypto" + bip32 "github.com/tyler-smith/go-bip32" + bip39 "github.com/tyler-smith/go-bip39" ) // GetL1DevnetChainArgsOrDefault extracts and formats the chain arguments for devnet. @@ -156,3 +161,51 @@ func GetL1RPCURL(basePort int) string { func GetL2RPCURL(basePort int) string { return fmt.Sprintf("http://localhost:%d", GetL2Port(basePort)) } + +// GetPrivateKeyFromMnemonic returns an ecdsa.PrivateKey from the index position on the mnemonic +func GetPrivateKeyFromMnemonic(mnemonic, passphrase string, index uint32) (*ecdsa.PrivateKey, error) { + // Validate mnemonic + if !bip39.IsMnemonicValid(strings.TrimSpace(mnemonic)) { + return nil, fmt.Errorf("invalid mnemonic") + } + + // BIP-39 seed + seed := bip39.NewSeed(mnemonic, passphrase) + + // BIP-32 master key + masterKey, err := bip32.NewMasterKey(seed) + if err != nil { + return nil, fmt.Errorf("new master key: %w", err) + } + + // Derivation path m/44'/60'/0'/0/index + const HardenedOffset = uint32(bip32.FirstHardenedChild) // 0x80000000 + purpose, err := masterKey.NewChildKey(44 + HardenedOffset) + if err != nil { + return nil, err + } + coinType, err := purpose.NewChildKey(60 + HardenedOffset) + if err != nil { + return nil, err + } + account, err := coinType.NewChildKey(0 + HardenedOffset) + if err != nil { + return nil, err + } + change, err := account.NewChildKey(0) // change = 0 + if err != nil { + return nil, err + } + addrKey, err := change.NewChildKey(index) // non-hardened index + if err != nil { + return nil, err + } + + // addrKey.Key should be 32 bytes private key material + if len(addrKey.Key) != 32 { + return nil, fmt.Errorf("unexpected key length: %d", len(addrKey.Key)) + } + + priv := crypto.ToECDSAUnsafe(addrKey.Key) + return priv, nil +} diff --git a/pkg/common/devnet/utils.go b/pkg/common/devnet/utils.go index 614167e1..bacc7064 100644 --- a/pkg/common/devnet/utils.go +++ b/pkg/common/devnet/utils.go @@ -332,3 +332,59 @@ func SyncL1L2Timestamps(ctx *cli.Context, l1RpcUrl string, l2RpcUrl string) erro // Already in sync return nil } + +// DetectSetStorageMethod returns "anvil_setStorageAt", "hardhat_setStorageAt", or "" if none found. +func DetectSetStorageMethod(rpcClient *rpc.Client) (string, error) { + // Quick sniff via client version + var clientVersion string + _ = rpcClient.Call(&clientVersion, "web3_clientVersion") + + lc := strings.ToLower(clientVersion) + if strings.Contains(lc, "anvil") || strings.Contains(lc, "foundry") || strings.Contains(lc, "reth") { + return "anvil_setStorageAt", nil + } + if strings.Contains(lc, "hardhat") { + return "hardhat_setStorageAt", nil + } + + // Fallback probe: call each method with no args to see whether the RPC server recognises it. + try := func(method string) (bool, string) { + var out interface{} + err := rpcClient.Call(&out, method) // intentionally no args + if err == nil { + // Method accepted and returned something without args + return true, "" + } + es := err.Error() + // Unknown-method / untagged enum style messages indicate unsupported method + // Accept anything that is NOT obviously "method not found" as evidence of support + unknownIndicators := []string{ + "did not match any variant", // rust nodes + "method not found", + "unknown method", + "unrecognized method", + } + for _, f := range unknownIndicators { + if strings.Contains(strings.ToLower(es), f) { + return false, es + } + } + // If error exists but does not match unknown indicators, treat it as "method exists but params bad" + return true, es + } + + // Prefer anvil first (default env), then hardhat + if ok, reason := try("anvil_setStorageAt"); ok { + return "anvil_setStorageAt", nil + } else { + // keep reason for debugging + _ = reason + } + if ok, reason := try("hardhat_setStorageAt"); ok { + return "hardhat_setStorageAt", nil + } else { + _ = reason + } + + return "", fmt.Errorf("no supported devnet setStorageAt method detected") +} diff --git a/pkg/common/getters.go b/pkg/common/getters.go index 2e8e66c5..aa3b7f6d 100644 --- a/pkg/common/getters.go +++ b/pkg/common/getters.go @@ -5,6 +5,30 @@ import ( "os" ) +func GetRPCUrlDefault(contextName string, cfg *ConfigWithContextConfig, chainName string) (string, error) { + // Check in env first for L1 RPC url + l1RPCUrl := os.Getenv("L1_RPC_URL") + if chainName == "l1" && l1RPCUrl != "" { + return l1RPCUrl, nil + } + + // Check in env first for L2 RPC url + l2RPCUrl := os.Getenv("L2_RPC_URL") + if chainName == "l2" && l2RPCUrl != "" { + return l2RPCUrl, nil + } + + // Fallback to context defined value + chainConfig, found := cfg.Context[contextName].Chains[chainName] + if !found { + return "", fmt.Errorf("failed to get chainConfig for chainName : %s", chainName) + } + if chainConfig.RPCURL == "" { + return "", fmt.Errorf("rpc-url not set for %s; set rpc-url in ./config/context/%s.yaml or .env and consult README for guidance", chainName, contextName) + } + return chainConfig.RPCURL, nil +} + func GetForkUrlDefault(contextName string, cfg *ConfigWithContextConfig, chainName string) (string, error) { // Check in env first for L1 fork url l1ForkUrl := os.Getenv("L1_FORK_URL") diff --git a/pkg/migration/migrator.go b/pkg/migration/migrator.go index eb4878dc..1c100954 100644 --- a/pkg/migration/migrator.go +++ b/pkg/migration/migrator.go @@ -328,6 +328,85 @@ func EnsureKeyWithComment(root *yaml.Node, path []string, comment string) { parent.Content = append(parent.Content, keyNode, valNode) } +// InsertAfterKeyWithComment inserts: +// +// # comment +// newKey: +// +// into the mapping at pathToMap, positioned immediately after afterKey. +// If afterKey is not found, it appends at the end. +// If overwrite is true and newKey already exists, it overwrites its value in place and updates the comment. +// Returns true if it mutated the tree. +func InsertAfterKeyWithComment( + root *yaml.Node, + pathToMap []string, + afterKey string, + newKey string, + newVal *yaml.Node, + comment string, + overwrite bool, +) bool { + if root == nil || len(pathToMap) == 0 { + return false + } + // Unwrap document node + if root.Kind == yaml.DocumentNode && len(root.Content) > 0 { + root = root.Content[0] + } + + parent := ResolveNode(root, pathToMap) + if parent == nil || parent.Kind != yaml.MappingNode { + return false + } + + // If key already exists + for i := 0; i < len(parent.Content)-1; i += 2 { + if parent.Content[i].Value == newKey { + if !overwrite { + return false + } + // Overwrite value, update comment on key + parent.Content[i].HeadComment = strings.TrimSpace(comment) + parent.Content[i+1] = CloneNode(newVal) + return true + } + } + + // Build key node with a head comment so it renders above the key + keyNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: newKey, + HeadComment: strings.TrimSpace(comment), + } + valNode := CloneNode(newVal) + if valNode == nil { + // default to null if caller passed nil + valNode = &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null"} + } + + // Find insertion point after afterKey + insertAt := -1 + for i := 0; i < len(parent.Content)-1; i += 2 { + if parent.Content[i].Value == afterKey { + insertAt = i + 2 // after the found key's value + break + } + } + if insertAt < 0 || insertAt > len(parent.Content) { + // append at end + parent.Content = append(parent.Content, keyNode, valNode) + return true + } + + // Splice in place: [..., insertAt-1] + [keyNode, valNode] + [insertAt...] + parent.Content = append(parent.Content, nil, nil) + copy(parent.Content[insertAt+2:], parent.Content[insertAt:]) + parent.Content[insertAt] = keyNode + parent.Content[insertAt+1] = valNode + return true +} + // findParent locates the parent mapping or sequence node and the index/key position func findParent(root *yaml.Node, path []string) (*yaml.Node, int) { if len(path) == 0 { diff --git a/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go b/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go new file mode 100644 index 00000000..a986c0ee --- /dev/null +++ b/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go @@ -0,0 +1,132 @@ +package migration_test + +import ( + "testing" + + "github.com/Layr-Labs/devkit-cli/config/contexts" + "github.com/Layr-Labs/devkit-cli/pkg/migration" + "gopkg.in/yaml.v3" +) + +// assumes testNode(t, yamlStr) helper exists in this package like in your other test +func TestMigration_0_1_1_to_0_1_2(t *testing.T) { + oldYAML := ` +version: 0.1.1 +context: + name: "devnet" + chains: + l1: + chain_id: 31337 + rpc_url: "http://localhost:8545" + fork: + block: 9259079 + url: "" + block_time: 3 + l2: + chain_id: 31338 + rpc_url: "http://localhost:9545" + fork: + block: 31408197 + url: "" + block_time: 3 +` + + userNode := testNode(t, oldYAML) + + // locate the 0.1.1 -> 0.1.2 step from the chain + var step migration.MigrationStep + for _, s := range contexts.MigrationChain { + if s.From == "0.1.1" && s.To == "0.1.2" { + step = s + break + } + } + if step.Apply == nil { + t.Fatalf("migration step 0.1.1 -> 0.1.2 not found") + } + + migrated, err := migration.MigrateNode(userNode, "0.1.1", "0.1.2", []migration.MigrationStep{step}) + if err != nil { + t.Fatalf("Migration failed: %v", err) + } + + t.Run("version bumped", func(t *testing.T) { + v := migration.ResolveNode(migrated, []string{"version"}) + if v == nil || v.Value != "0.1.2" { + t.Errorf("expected version 0.1.2, got %v", v) + } + }) + + t.Run("mnemonic inserted with value", func(t *testing.T) { + // value node + val := migration.ResolveNode(migrated, []string{"context", "mnumonic"}) + if val == nil { + t.Fatalf("mnumonic key missing") + } + want := "test test test test test test test test test test test junk" + if val.Value != want { + t.Errorf("expected mnumonic value %q, got %q", want, val.Value) + } + }) + + t.Run("inserted after context.chains", func(t *testing.T) { + // inspect ordering within the context mapping + ctx := migration.ResolveNode(migrated, []string{"context"}) + if ctx == nil || ctx.Kind != 4 /* yaml.ScalarNode */ { + t.Fatalf("context mapping missing or wrong kind %d", ctx.Kind) + } + chainsIdx := -1 + mnumonicIdx := -1 + for i := 0; i < len(ctx.Content)-1; i += 2 { + k := ctx.Content[i] + switch k.Value { + case "chains": + chainsIdx = i + case "mnumonic": + mnumonicIdx = i + } + } + if chainsIdx < 0 { + t.Fatalf("context.chains key not found") + } + if mnumonicIdx < 0 { + t.Fatalf("context.mnumonic key not found") + } + if mnumonicIdx != chainsIdx+2 { + t.Errorf("mnumonic not inserted immediately after chains: chainsIdx=%d mnumonicIdx=%d", chainsIdx, mnumonicIdx) + } + }) + + t.Run("comment attached to mnumonic key", func(t *testing.T) { + ctx := migration.ResolveNode(migrated, []string{"context"}) + var keyNode *yaml.Node + for i := 0; i < len(ctx.Content)-1; i += 2 { + if ctx.Content[i].Value == "mnumonic" { + keyNode = ctx.Content[i] + break + } + } + if keyNode == nil { + t.Fatalf("mnumonic key node not found") + } + wantComment := "Devnet mnemonic for unlocked accounts" + if keyNode.HeadComment != wantComment { + t.Errorf("expected head comment %q, got %q", wantComment, keyNode.HeadComment) + } + }) + + t.Run("other fields preserved", func(t *testing.T) { + nameNode := migration.ResolveNode(migrated, []string{"context", "name"}) + if nameNode == nil || nameNode.Value != "devnet" { + t.Errorf("expected name preserved as devnet, got %v", nameNode) + } + l1ChainId := migration.ResolveNode(migrated, []string{"context", "chains", "l1", "chain_id"}) + if l1ChainId == nil || l1ChainId.Value != "31337" { + t.Errorf("expected L1 chain_id 31337, got %v", l1ChainId) + } + l2ChainId := migration.ResolveNode(migrated, []string{"context", "chains", "l2", "chain_id"}) + if l2ChainId == nil || l2ChainId.Value != "31338" { + t.Errorf("expected L2 chain_id 31338, got %v", l2ChainId) + } + }) +} From 517b9c60a7973b433d2f15a11b7de3f0c02835e8 Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:48:18 +0000 Subject: [PATCH 02/10] fix: ensure all wallets are funded regardless of mnemonic used --- pkg/commands/devnet_actions.go | 2 +- pkg/common/devnet/funding.go | 90 +++++++++++++++++++++++++------- pkg/common/devnet/utils.go | 93 +++++++++++++++++++--------------- 3 files changed, 125 insertions(+), 60 deletions(-) diff --git a/pkg/commands/devnet_actions.go b/pkg/commands/devnet_actions.go index 9de1197c..a8a014b2 100644 --- a/pkg/commands/devnet_actions.go +++ b/pkg/commands/devnet_actions.go @@ -1269,7 +1269,7 @@ func SetAllocationDelayAction(cCtx *cli.Context, logger iface.Logger) error { } defer client.Close() - method, err := devnet.DetectSetStorageMethod(client.Client()) + method, err := devnet.DetectClientsMethod(cCtx.Context, client.Client(), "setStorageAt") if err != nil { return fmt.Errorf("failed to detect setStorageAt method: %w", err) } diff --git a/pkg/common/devnet/funding.go b/pkg/common/devnet/funding.go index 435f1721..c4d7bd23 100644 --- a/pkg/common/devnet/funding.go +++ b/pkg/common/devnet/funding.go @@ -1,6 +1,7 @@ package devnet import ( + "bytes" "context" "crypto/ecdsa" "fmt" @@ -48,6 +49,13 @@ var DefaultTokenHolders = map[common.Address]TokenFunding{ // FundStakerWithTokens funds staker with strategy tokens using impersonation func FundStakerWithTokens(ctx context.Context, ethClient *ethclient.Client, rpcClient *rpc.Client, stakerAddress common.Address, tokenFunding TokenFunding, tokenAddress common.Address, mnemonic string, rpcURL string) error { + // Fund the staker if required + err := FundIfNeeded(ethClient, stakerAddress, mnemonic) + if err != nil { + return err + } + + // Switch funding strategy based on funding token if tokenFunding.TokenName == "bEIGEN" { // For bEIGEN, we need to call unwrap() on the EIGEN contract first // to convert EIGEN tokens to bEIGEN tokens @@ -335,11 +343,22 @@ func FundWalletsDevnet(cfg *devkitcommon.ConfigWithContextConfig, rpcURL string, // Fund deployer deployerPrivateKey := cfg.Context[DEVNET_CONTEXT].DeployerPrivateKey - deployedECDSAKey, err := crypto.HexToECDSA(strings.TrimPrefix(deployerPrivateKey, "0x")) + deployerECDSAKey, err := crypto.HexToECDSA(strings.TrimPrefix(deployerPrivateKey, "0x")) if err != nil { log.Fatalf("invalid private key %q: %v", deployerPrivateKey, err) } - err = FundIfNeeded(ethClient, crypto.PubkeyToAddress(deployedECDSAKey.PublicKey), mnemonic) + err = FundIfNeeded(ethClient, crypto.PubkeyToAddress(deployerECDSAKey.PublicKey), mnemonic) + if err != nil { + return err + } + + // Fund app + appDeployerPrivateKey := cfg.Context[DEVNET_CONTEXT].AppDeployerPrivateKey + appDeployerECDSAKey, err := crypto.HexToECDSA(strings.TrimPrefix(appDeployerPrivateKey, "0x")) + if err != nil { + log.Fatalf("invalid private key %q: %v", appDeployerPrivateKey, err) + } + err = FundIfNeeded(ethClient, crypto.PubkeyToAddress(appDeployerECDSAKey.PublicKey), mnemonic) if err != nil { return err } @@ -405,8 +424,13 @@ func FundIfNeeded(ethClient *ethclient.Client, to common.Address, mnemonic strin return fmt.Errorf("failed to get sender balance: %w", err) } + // Check if the address we are sending to holds an EOA forwarder + if err := clearCodeIfEOAForwarder(context.Background(), ethClient.Client(), ethClient, to); err != nil { + return fmt.Errorf("clearCodeIfEOAForwarder: %w", err) + } + // Calculate total cost (value + gas (large gas limit here to allow for fallbacks when contract)) - gasLimit := uint64(30000) + gasLimit := uint64(42000) totalCost := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasLimit))) totalCost.Add(totalCost, value) @@ -446,7 +470,7 @@ func FundIfNeeded(ethClient *ethclient.Client, to common.Address, mnemonic strin return fmt.Errorf("failed to send transaction: %w", err) } - log.Printf("Transaction sent at nonce %d from %s with val %d (tx: %s), waiting for confirmation...", nonce, fromAddress, value, signedTx.Hash().Hex()) + log.Printf("Transaction sent (tx: %s), waiting for confirmation...", signedTx.Hash().Hex()) // Wait for transaction to be mined using bind.WaitMined receipt, err := bind.WaitMined(context.Background(), ethClient, signedTx) @@ -458,20 +482,6 @@ func FundIfNeeded(ethClient *ethclient.Client, to common.Address, mnemonic strin return fmt.Errorf("transaction failed") } - minedTx, _, err := ethClient.TransactionByHash(context.Background(), signedTx.Hash()) - if err != nil { - return err - } - if minedTx.To() == nil || *minedTx.To() != to { - return fmt.Errorf("unexpected tx.to: want %s got %v", to.Hex(), minedTx.To()) - } - if minedTx.Value().Cmp(value) != 0 { - return fmt.Errorf("unexpected tx value: want %s got %s", value, minedTx.Value()) - } - - newBal, _ := ethClient.BalanceAt(context.Background(), to, nil) - log.Printf("recipient balance after: %s wei", newBal.String()) - log.Printf("✅ Funded %s (tx: %s)", to, signedTx.Hash().Hex()) return nil } @@ -551,3 +561,47 @@ func GetUnderlyingTokenAddressesFromStrategies(cfg *devkitcommon.ConfigWithConte return tokenAddresses, nil } + +// isKnownEOAForwarder detects EOA acting as a delegated smart-contact (EIP-7702) +func isKnownEOAForwarder(code []byte) bool { + if len(code) == 0 { + return false + } + if bytes.Contains(code, common.FromHex("0xef0100")) { + return true + } + return false +} + +// clearCodeIfEOAForwarder clears code for any EOA forwarder +func clearCodeIfEOAForwarder(ctx context.Context, rpcClient *rpc.Client, ethClient *ethclient.Client, addr common.Address) error { + code, err := ethClient.CodeAt(ctx, addr, nil) + if err != nil { + return err + } + if !isKnownEOAForwarder(code) { + return nil + } + + // Detect supported method in devnet environment + method, err := DetectClientsMethod(ctx, rpcClient, "setCode") + if err != nil { + return fmt.Errorf("failed to detect setCode method: %w", err) + } + + var res interface{} + // Set code to empty + if err := rpcClient.CallContext(ctx, &res, method, addr.Hex(), "0x"); err != nil { + return err + } + + // sanity check + code2, err := ethClient.CodeAt(ctx, addr, nil) + if err != nil { + return err + } + if len(code2) != 0 { + return fmt.Errorf("failed to clear code at %s", addr.Hex()) + } + return nil +} diff --git a/pkg/common/devnet/utils.go b/pkg/common/devnet/utils.go index bacc7064..d5a1f28a 100644 --- a/pkg/common/devnet/utils.go +++ b/pkg/common/devnet/utils.go @@ -2,6 +2,8 @@ package devnet import ( "context" + "encoding/json" + "errors" "fmt" "net" "net/url" @@ -333,58 +335,67 @@ func SyncL1L2Timestamps(ctx *cli.Context, l1RpcUrl string, l2RpcUrl string) erro return nil } -// DetectSetStorageMethod returns "anvil_setStorageAt", "hardhat_setStorageAt", or "" if none found. -func DetectSetStorageMethod(rpcClient *rpc.Client) (string, error) { - // Quick sniff via client version +// DetectClientsMethod returns "anvil_", "hardhat_", or "" if unsupported. +func DetectClientsMethod(ctx context.Context, c *rpc.Client, method string) (string, error) { + // Fast path via clientVersion var clientVersion string - _ = rpcClient.Call(&clientVersion, "web3_clientVersion") - + _ = c.CallContext(ctx, &clientVersion, "web3_clientVersion") lc := strings.ToLower(clientVersion) - if strings.Contains(lc, "anvil") || strings.Contains(lc, "foundry") || strings.Contains(lc, "reth") { - return "anvil_setStorageAt", nil - } if strings.Contains(lc, "hardhat") { - return "hardhat_setStorageAt", nil + return "hardhat_" + method, nil + } + if strings.Contains(lc, "anvil") || strings.Contains(lc, "foundry") { + return "anvil_" + method, nil + } + + // Harmless identity probes + type q struct{ name string } + ids := []q{{"anvil_nodeInfo"}, {"hardhat_metadata"}} + batch := make([]rpc.BatchElem, 0, len(ids)) + for i := range ids { + batch = append(batch, rpc.BatchElem{ + Method: ids[i].name, + Args: []any{}, + Result: new(json.RawMessage), + }) + } + _ = c.BatchCallContext(ctx, batch) + for i, el := range batch { + if el.Error == nil { + switch ids[i].name { + case "anvil_nodeInfo": + return "anvil_" + method, nil + case "hardhat_metadata": + return "hardhat_" + method, nil + } + } } - // Fallback probe: call each method with no args to see whether the RPC server recognises it. - try := func(method string) (bool, string) { - var out interface{} - err := rpcClient.Call(&out, method) // intentionally no args + // Direct capability probes. Call with no args to avoid unwanted side-effects + supports := func(prefix string) bool { + var out any + err := c.CallContext(ctx, &out, prefix+"_"+method) if err == nil { - // Method accepted and returned something without args - return true, "" + return true } - es := err.Error() - // Unknown-method / untagged enum style messages indicate unsupported method - // Accept anything that is NOT obviously "method not found" as evidence of support - unknownIndicators := []string{ - "did not match any variant", // rust nodes - "method not found", - "unknown method", - "unrecognized method", + var rerr rpc.Error + if errors.As(err, &rerr) { + // −32601 is method not found. Anything else means method exists but args invalid. + return rerr.ErrorCode() != -32601 } - for _, f := range unknownIndicators { - if strings.Contains(strings.ToLower(es), f) { - return false, es - } + // Fallback textual check only if not an *rpc.Error. + es := strings.ToLower(err.Error()) + if strings.Contains(es, "method not found") || strings.Contains(es, "unknown method") || strings.Contains(es, "unrecognized method") { + return false } - // If error exists but does not match unknown indicators, treat it as "method exists but params bad" - return true, es + return true } - // Prefer anvil first (default env), then hardhat - if ok, reason := try("anvil_setStorageAt"); ok { - return "anvil_setStorageAt", nil - } else { - // keep reason for debugging - _ = reason + if supports("anvil") { + return "anvil_" + method, nil } - if ok, reason := try("hardhat_setStorageAt"); ok { - return "hardhat_setStorageAt", nil - } else { - _ = reason + if supports("hardhat") { + return "hardhat_" + method, nil } - - return "", fmt.Errorf("no supported devnet setStorageAt method detected") + return "", nil } From 43512536f3107b5f08ca31afee2678e23cfd7dfb Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:58:00 +0000 Subject: [PATCH 03/10] fix: wait for addChainIDsToWhitelist receipt --- pkg/commands/devnet_actions.go | 1 - pkg/common/contract_caller.go | 83 ++++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/pkg/commands/devnet_actions.go b/pkg/commands/devnet_actions.go index a8a014b2..47c6cf89 100644 --- a/pkg/commands/devnet_actions.go +++ b/pkg/commands/devnet_actions.go @@ -1424,7 +1424,6 @@ func WhitelistChainIdInCrossRegistryAction(cCtx *cli.Context, logger iface.Logge return fmt.Errorf("failed to whitelist l2 ChainId in CrossChainRegistry: %w", err) } - logger.Info("Successfully whitelisted l1 chain id in cross registry") return nil } diff --git a/pkg/common/contract_caller.go b/pkg/common/contract_caller.go index 9c1893e9..66c9b460 100644 --- a/pkg/common/contract_caller.go +++ b/pkg/common/contract_caller.go @@ -6,9 +6,11 @@ import ( "crypto/ecdsa" "encoding/hex" "encoding/json" + "errors" "fmt" "math/big" "strings" + "time" "github.com/Layr-Labs/devkit-cli/pkg/common/contracts" "github.com/Layr-Labs/devkit-cli/pkg/common/iface" @@ -17,6 +19,7 @@ import ( "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/DelegationManager" keyregistrar "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/KeyRegistrar" releasemanager "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/ReleaseManager" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -627,6 +630,38 @@ func (cc *ContractCaller) CreateGenerationReservation(ctx context.Context, opSet return err } +func (cc *ContractCaller) AlreadyWhitelisted(ctx context.Context, chainId uint64) (bool, error) { + parsed, err := crosschainregistry.CrossChainRegistryMetaData.GetAbi() + if err != nil { + return false, err + } + + data, err := parsed.Pack("getSupportedChains") + if err != nil { + return false, err + } + + out, err := cc.ethclient.CallContract(ctx, ethereum.CallMsg{ + To: &cc.crossChainRegistryAddr, + Data: data, + }, nil) + if err != nil { + return false, err + } + + var ids []*big.Int + var addrs []common.Address + if err := parsed.UnpackIntoInterface(&[]interface{}{&ids, &addrs}, "getSupportedChains", out); err != nil { + return false, err + } + for _, id := range ids { + if id.Uint64() == chainId { + return true, nil + } + } + return false, nil +} + func (cc *ContractCaller) WhitelistChainIdInCrossRegistry(ctx context.Context, operatorTableUpdater common.Address, chainId uint64) error { var ( err error @@ -634,17 +669,28 @@ func (cc *ContractCaller) WhitelistChainIdInCrossRegistry(ctx context.Context, o receipt *types.Receipt ) - chainIds := []*big.Int{big.NewInt(int64(chainId))} - cc.logger.Info("Impersonating cross chain registry owner") - ownerCrossChainRegistry := common.HexToAddress(CrossChainRegistryOwnerAddress) + // Avoid attempting already whitelisted ids + ok, err := cc.AlreadyWhitelisted(ctx, chainId) + if err != nil { + return err + } + if ok { + cc.logger.Info("Chain %d already whitelisted - skipping", chainId) + return nil + } // Get RPC client from ethclient rpcClient := cc.ethclient.Client() + // Pack chainIds for addChainIDsToWhitelist + chainIds := []*big.Int{big.NewInt(int64(chainId))} + + // Impersonate the owner (is a smart-contract - eip-3607 must be disabled) + cc.logger.Info("Impersonating cross chain registry owner") + ownerCrossChainRegistry := common.HexToAddress(CrossChainRegistryOwnerAddress) if err := ImpersonateAccount(rpcClient, ownerCrossChainRegistry); err != nil { return fmt.Errorf("failed to impersonate account: %w", err) } - defer func() { if err := StopImpersonatingAccount(rpcClient, ownerCrossChainRegistry); err != nil { cc.logger.Error("failed to stop impersonating account: %w", err) @@ -685,18 +731,11 @@ func (cc *ContractCaller) WhitelistChainIdInCrossRegistry(ctx context.Context, o } // Force the tx to be mined - err = rpcClient.Call(nil, "evm_mine") + receipt, err = cc.WaitReceiptByHash(ctx, cc.ethclient, txHash) if err != nil { - return fmt.Errorf("evm_mine call failed: %w", err) + cc.logger.Error("Waiting for addChainIDsToWhitelist transaction (hash: %s) failed: %v", txHash.Hex(), err) + return fmt.Errorf("waiting for addChainIDsToWhitelist transaction (hash: %s): %w", txHash.Hex(), err) } - - // Wait for transaction receipt - receipt, err = cc.ethclient.TransactionReceipt(ctx, txHash) - if err != nil { - cc.logger.Error("failed to get transaction receipt: %w", err) - return fmt.Errorf("addChainIDsToWhitelist transaction failed: %w", err) - } - // Check for reverted tx and print receipt if receipt.Status == 0 { jsonBytes, err := json.MarshalIndent(receipt, "", " ") @@ -833,3 +872,19 @@ func (cc *ContractCaller) PackUint256Pair(x, y *big.Int) ([]byte, error) { } return args.Pack(x, y) } + +// Wait for a tx to be mined by hash +func (cc *ContractCaller) WaitReceiptByHash(ctx context.Context, c *ethclient.Client, h common.Hash) (*types.Receipt, error) { + for { + r, err := c.TransactionReceipt(ctx, h) + if errors.Is(err, ethereum.NotFound) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(time.Second): + continue + } + } + return r, err + } +} From 8a3edb3bc5e473ea74c10592cc9aba771b970cd7 Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:58:17 +0000 Subject: [PATCH 04/10] fix: use latestBlock for reference timestamp --- pkg/commands/transporter.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/commands/transporter.go b/pkg/commands/transporter.go index c8938d5a..7f65a6d0 100644 --- a/pkg/commands/transporter.go +++ b/pkg/commands/transporter.go @@ -249,7 +249,11 @@ func Transport(cCtx *cli.Context, initialRun bool) error { } } - l1Block, err := l1Client.RPCClient.BlockByNumber(cCtx.Context, big.NewInt(int64(rpc.FinalizedBlockNumber))) + l1LatestBlockNumber, err := l1Client.RPCClient.BlockNumber(cCtx.Context) + if err != nil { + return fmt.Errorf("failed to get latest block number for l1: %v", err) + } + l1Block, err := l1Client.RPCClient.BlockByNumber(cCtx.Context, big.NewInt(int64(l1LatestBlockNumber))) if err != nil { return fmt.Errorf("failed to get block by number for l1: %v", err) } From 71fb86ed000c57d543a802478337370de92bd1a8 Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:04:37 +0000 Subject: [PATCH 05/10] fix: token funding back to 1000 --- pkg/common/devnet/funding.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/common/devnet/funding.go b/pkg/common/devnet/funding.go index c4d7bd23..26fa9992 100644 --- a/pkg/common/devnet/funding.go +++ b/pkg/common/devnet/funding.go @@ -38,12 +38,12 @@ var DefaultTokenHolders = map[common.Address]TokenFunding{ common.HexToAddress(ST_ETH_TOKEN_ADDRESS): { // stETH token address TokenName: "stETH", HolderAddress: common.HexToAddress("0xC8088abD2FdaF4819230EB0FdA2D9766FDF9F409"), // Large stETH holder - Amount: new(big.Int).Mul(big.NewInt(STRATEGY_TOKEN_FUNDING_AMOUNT_BY_LARGE_HOLDER_IN_ETH), big.NewInt(1e17)), // .1 tokens + Amount: new(big.Int).Mul(big.NewInt(STRATEGY_TOKEN_FUNDING_AMOUNT_BY_LARGE_HOLDER_IN_ETH), big.NewInt(1e18)), // 1000 tokens }, common.HexToAddress(B_EIGEN_TOKEN_ADDRESS): { // bEIGEN token address TokenName: "bEIGEN", HolderAddress: common.HexToAddress("0x5f8C207382426D3f7F248E6321Cf93B34e66d6b9"), // Large EIGEN holder that calls unwrap() to get bEIGEN - Amount: new(big.Int).Mul(big.NewInt(STRATEGY_TOKEN_FUNDING_AMOUNT_BY_LARGE_HOLDER_IN_ETH), big.NewInt(1e17)), // .1 tokens + Amount: new(big.Int).Mul(big.NewInt(STRATEGY_TOKEN_FUNDING_AMOUNT_BY_LARGE_HOLDER_IN_ETH), big.NewInt(1e18)), // 1000 tokens }, } From a25717125ac5cdae86412fadfd6619f2df08ab11 Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:35:26 +0000 Subject: [PATCH 06/10] fix: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02a4ba12..b7792fba 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,7 @@ DevNet management commands: | `stop --port` | Stops the specific port e.g.: `stop --port 8545` | -Alternatively, `devnet` can be started against an already running forked enviorment (eg, local anvil instances or from remote BuildBear sandboxes). This will allow you to interact with a testnet without spinning up or spinning down the onchain environment each iteration. +Alternatively, `devnet` can be started against an already running forked environment (eg, local anvil instances or from remote BuildBear sandboxes). This will allow you to interact with a testnet without spinning up or spinning down the onchain environment each iteration. Start/create your own forked sepolia instances and in your project directory, set the appropriate `rpc_urls` in your context and run: From 222dc164cf6236c7a263f03d1a30b336d720235d Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:40:27 +0000 Subject: [PATCH 07/10] fix: linting and test typos --- pkg/commands/devnet_actions.go | 8 +++--- pkg/commands/transporter.go | 2 +- .../avs_context_0_1_1_to_0_1_2_test.go | 26 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/commands/devnet_actions.go b/pkg/commands/devnet_actions.go index 47c6cf89..8119da78 100644 --- a/pkg/commands/devnet_actions.go +++ b/pkg/commands/devnet_actions.go @@ -74,7 +74,7 @@ func StartDevnetAction(cCtx *cli.Context) error { } // Check if docker is running, else try to start it - if skipForking == false { + if !skipForking { if err := common.EnsureDockerIsRunning(cCtx); err != nil { if errors.Is(err, context.Canceled) { @@ -141,7 +141,7 @@ func StartDevnetAction(cCtx *cli.Context) error { l2ChainDescription := fmt.Sprintf("against RPC %s", l2RpcUrl) // Start anvil containers - if skipForking == false { + if !skipForking { l1Port := cCtx.Int("l1-port") l2Port := cCtx.Int("l2-port") @@ -270,7 +270,7 @@ func StartDevnetAction(cCtx *cli.Context) error { // On cancel, stop the containers if we're not skipping deployContracts/avsRun and we're not persisting if !skipDeployContracts && !skipAvsRun && !persist { defer func() { - if skipForking == false { + if !skipForking { logger.Info("Stopping containers") // Use background context to avoid cancellation issues during cleanup bgCtx := context.Background() @@ -372,7 +372,7 @@ func StartDevnetAction(cCtx *cli.Context) error { // Deploy the contracts after starting devnet unless skipped if !skipDeployContracts { // We only need docker if we're forking locally - if skipForking == false { + if !skipForking { // Check if docker is running, else try to start it err := common.EnsureDockerIsRunning(cCtx) if err != nil { diff --git a/pkg/commands/transporter.go b/pkg/commands/transporter.go index 7f65a6d0..74290016 100644 --- a/pkg/commands/transporter.go +++ b/pkg/commands/transporter.go @@ -504,7 +504,7 @@ func Transport(cCtx *cli.Context, initialRun bool) error { for _, opset := range opsets { // Only transport the configured AVSs operatorSets - if strings.ToLower(opset.Avs.Hex()) == strings.ToLower(envCtx.Avs.Address) { + if strings.EqualFold(opset.Avs.Hex(), envCtx.Avs.Address) { err = stakeTransport.SignAndTransportAvsStakeTable( cCtx.Context, referenceTimestamp, diff --git a/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go b/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go index a986c0ee..4bfda998 100644 --- a/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go +++ b/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go @@ -59,13 +59,13 @@ context: t.Run("mnemonic inserted with value", func(t *testing.T) { // value node - val := migration.ResolveNode(migrated, []string{"context", "mnumonic"}) + val := migration.ResolveNode(migrated, []string{"context", "mnemonic"}) if val == nil { - t.Fatalf("mnumonic key missing") + t.Fatalf("mnemonic key missing") } want := "test test test test test test test test test test test junk" if val.Value != want { - t.Errorf("expected mnumonic value %q, got %q", want, val.Value) + t.Errorf("expected mnemonic value %q, got %q", want, val.Value) } }) @@ -76,38 +76,38 @@ context: t.Fatalf("context mapping missing or wrong kind %d", ctx.Kind) } chainsIdx := -1 - mnumonicIdx := -1 + mnemonicIdx := -1 for i := 0; i < len(ctx.Content)-1; i += 2 { k := ctx.Content[i] switch k.Value { case "chains": chainsIdx = i - case "mnumonic": - mnumonicIdx = i + case "mnemonic": + mnemonicIdx = i } } if chainsIdx < 0 { t.Fatalf("context.chains key not found") } - if mnumonicIdx < 0 { - t.Fatalf("context.mnumonic key not found") + if mnemonicIdx < 0 { + t.Fatalf("context.mnemonic key not found") } - if mnumonicIdx != chainsIdx+2 { - t.Errorf("mnumonic not inserted immediately after chains: chainsIdx=%d mnumonicIdx=%d", chainsIdx, mnumonicIdx) + if mnemonicIdx != chainsIdx+2 { + t.Errorf("mnemonic not inserted immediately after chains: chainsIdx=%d mnemonicIdx=%d", chainsIdx, mnemonicIdx) } }) - t.Run("comment attached to mnumonic key", func(t *testing.T) { + t.Run("comment attached to mnemonic key", func(t *testing.T) { ctx := migration.ResolveNode(migrated, []string{"context"}) var keyNode *yaml.Node for i := 0; i < len(ctx.Content)-1; i += 2 { - if ctx.Content[i].Value == "mnumonic" { + if ctx.Content[i].Value == "mnemonic" { keyNode = ctx.Content[i] break } } if keyNode == nil { - t.Fatalf("mnumonic key node not found") + t.Fatalf("mnemonic key node not found") } wantComment := "Devnet mnemonic for unlocked accounts" if keyNode.HeadComment != wantComment { From 0effcb9e31642cbdf9d831ca2dbf53bae45fbc5c Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Wed, 10 Dec 2025 03:01:42 +0000 Subject: [PATCH 08/10] fix: generate new privateKey for transporter owner to avoid keyRegistrar conflicts --- config/contexts/migrations/v0.1.1-v0.1.2.go | 19 +++++- config/contexts/v0.1.2.yaml | 2 - pkg/commands/transporter.go | 68 +++++++++++++------ .../avs_context_0_1_1_to_0_1_2_test.go | 24 ++++++- 4 files changed, 89 insertions(+), 24 deletions(-) diff --git a/config/contexts/migrations/v0.1.1-v0.1.2.go b/config/contexts/migrations/v0.1.1-v0.1.2.go index 62e475ae..cfffa3df 100644 --- a/config/contexts/migrations/v0.1.1-v0.1.2.go +++ b/config/contexts/migrations/v0.1.1-v0.1.2.go @@ -10,7 +10,24 @@ import ( func Migration_0_1_1_to_0_1_2(user, old, new *yaml.Node) (*yaml.Node, error) { // Update fork block heights to match ponos project - engine := migration.PatchEngine{} + engine := migration.PatchEngine{ + Old: old, + New: new, + User: user, + Rules: []migration.PatchRule{ + // Remove transporter keys + { + Path: []string{"context", "transporter", "private_key"}, + Condition: migration.Always{}, + Remove: true, + }, + { + Path: []string{"context", "transporter", "bls_private_key"}, + Condition: migration.Always{}, + Remove: true, + }, + }, + } if err := engine.Apply(); err != nil { return nil, err } diff --git a/config/contexts/v0.1.2.yaml b/config/contexts/v0.1.2.yaml index bb35585c..fa919572 100644 --- a/config/contexts/v0.1.2.yaml +++ b/config/contexts/v0.1.2.yaml @@ -24,8 +24,6 @@ context: # Stake Root Transporter configuration transporter: schedule: "0 */2 * * *" - private_key: "0x5f8e6420b9cb0c940e3d3f8b99177980785906d16fb3571f70d7a05ecf5f2172" - bls_private_key: "0x5f8e6420b9cb0c940e3d3f8b99177980785906d16fb3571f70d7a05ecf5f2172" active_stake_roots: [] # All key material (BLS and ECDSA) within this file should be used for local testing ONLY # ECDSA keys used are from Anvil's private key set diff --git a/pkg/commands/transporter.go b/pkg/commands/transporter.go index 74290016..6485edca 100644 --- a/pkg/commands/transporter.go +++ b/pkg/commands/transporter.go @@ -16,6 +16,7 @@ import ( "github.com/Layr-Labs/devkit-cli/pkg/common/iface" "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/ICrossChainRegistry" "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IOperatorTableUpdater" + "github.com/tyler-smith/go-bip39" "github.com/urfave/cli/v2" "gopkg.in/yaml.v3" @@ -166,10 +167,8 @@ func Transport(cCtx *cli.Context, initialRun bool) error { return fmt.Errorf("context '%s' not found in configuration", contextName) } - // Debug logging to check what's loaded - logger.Info("Transporter config loaded - Private key present: %v, BLS key present: %v", - envCtx.Transporter.PrivateKey != "", - envCtx.Transporter.BlsPrivateKey != "") + // Extract devnet mnemonic + mnemonic := envCtx.Mnemonic // Get the values from env/config crossChainRegistryAddress := ethcommon.HexToAddress(envCtx.EigenLayer.L1.CrossChainRegistry) @@ -223,12 +222,46 @@ func Transport(cCtx *cli.Context, initialRun bool) error { return fmt.Errorf("failed to get l1 chain for ID %d: %v", l1Config.ChainID, err) } - // Check if private key is empty - if envCtx.Transporter.PrivateKey == "" { - return fmt.Errorf("transporter private key is empty. Please check config/contexts/devnet.yaml") + // Pull privateKey from random mnemonic to use as transporter owner + entropy, err := bip39.NewEntropy(256) + if err != nil { + return fmt.Errorf("entropy generation failed: %w", err) + } + randomMnemonic, err := bip39.NewMnemonic(entropy) + if err != nil { + return fmt.Errorf("mnemonic generation failed: %w", err) + } + privateKey, err := devnet.GetPrivateKeyFromMnemonic(randomMnemonic, "", 0) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + + // Convert ecdsaPrivateKey to hex + privateKeyHex := fmt.Sprintf("0x%x", privateKey.D.Bytes()) + publicKeyHex := crypto.PubkeyToAddress(privateKey.PublicKey) + + // Connect to an ethClient to fund transporter owner + l1EthClient, err := ethclient.Dial(l1RpcUrl) + if err != nil { + return fmt.Errorf("failed to connect to L1 RPC: %w", err) + } + l2EthClient, err := ethclient.Dial(l2RpcUrl) + if err != nil { + return fmt.Errorf("failed to connect to L2 RPC: %w", err) } - txSign, err := txSigner.NewPrivateKeySigner(envCtx.Transporter.PrivateKey) + // Fund transporter owner on both L1 and L2 + err = devnet.FundIfNeeded(l1EthClient, publicKeyHex, mnemonic) + if err != nil { + return fmt.Errorf("failed to fund %s", publicKeyHex) + } + err = devnet.FundIfNeeded(l2EthClient, publicKeyHex, mnemonic) + if err != nil { + return fmt.Errorf("failed to fund %s", publicKeyHex) + } + + // Create signer for transporter owner + txSign, err := txSigner.NewPrivateKeySigner(privateKeyHex) if err != nil { return fmt.Errorf("failed to create private key signer: %v", err) } @@ -265,13 +298,8 @@ func Transport(cCtx *cli.Context, initialRun bool) error { return fmt.Errorf("failed to calculate stake table root: %v", err) } - // Check if BLS private key is empty - if envCtx.Transporter.BlsPrivateKey == "" { - return fmt.Errorf("transporter BLS private key is empty. Please check config/contexts/devnet.yaml") - } - scheme := bn254.NewScheme() - genericPk, err := scheme.NewPrivateKeyFromHexString(envCtx.Transporter.BlsPrivateKey) + genericPk, err := scheme.NewPrivateKeyFromHexString(privateKeyHex) if err != nil { return fmt.Errorf("failed to create BLS private key: %v", err) } @@ -288,7 +316,7 @@ func Transport(cCtx *cli.Context, initialRun bool) error { // On initial devnet Transport we take ownership of contracts and configure generator to use context keys if contextName == devnet.DEVNET_CONTEXT && initialRun { // Transfer ownership to our context configured PrivateKey - transferOwnership(logger, l1Config.RPCURL, crossChainRegistryAddress, envCtx.Transporter.PrivateKey) + transferOwnership(logger, l1Config.RPCURL, crossChainRegistryAddress, privateKeyHex) // Construct registry caller ccRegistryCaller, err := ICrossChainRegistry.NewICrossChainRegistryCaller(crossChainRegistryAddress, l1Client.RPCClient) @@ -320,7 +348,7 @@ func Transport(cCtx *cli.Context, initialRun bool) error { if chainId.Uint64() == uint64(l2ChainId) { rpcURL = l2Config.RPCURL } - transferOwnership(logger, rpcURL, tableUpdaterAddr, envCtx.Transporter.PrivateKey) + transferOwnership(logger, rpcURL, tableUpdaterAddr, privateKeyHex) // Read the current generator (avs,id) from OperatorTableUpdater gen, err := getGenerator(cCtx.Context, logger, cm, chainId, tableUpdaterAddr) @@ -343,7 +371,7 @@ func Transport(cCtx *cli.Context, initialRun bool) error { // Construct contractCaller with KeyRegistrar contractCaller, err := common.NewContractCaller( - envCtx.Transporter.PrivateKey, + privateKeyHex, big.NewInt(int64(l1ChainId)), client, ethcommon.HexToAddress(""), @@ -360,7 +388,7 @@ func Transport(cCtx *cli.Context, initialRun bool) error { } // Derive BN254 keys from the hex string (no keystore files needed) - blsHex := strings.TrimPrefix(envCtx.Transporter.BlsPrivateKey, "0x") + blsHex := strings.TrimPrefix(privateKeyHex, "0x") // Extract key details scheme := bn254.NewScheme() @@ -394,7 +422,7 @@ func Transport(cCtx *cli.Context, initialRun bool) error { } // EOA/operator address you want to register for this OperatorSet - opEOA := mustKey(logger, envCtx.Transporter.PrivateKey) + opEOA := mustKey(logger, privateKeyHex) operatorAddress := crypto.PubkeyToAddress(opEOA.PublicKey) // Build the message hash per registrar rules and sign with BLS private key @@ -444,7 +472,7 @@ func Transport(cCtx *cli.Context, initialRun bool) error { certificateVerifierAddr := readBN254CertificateVerifier(cCtx.Context, logger, rpcURL, tableUpdaterAddr) // Update generator using the transporter BLS key - if err := updateGeneratorFromContext(cCtx.Context, logger, cm, chainId, tableUpdaterAddr, certificateVerifierAddr, txSign, envCtx.Transporter.BlsPrivateKey, gen); err != nil { + if err := updateGeneratorFromContext(cCtx.Context, logger, cm, chainId, tableUpdaterAddr, certificateVerifierAddr, txSign, privateKeyHex, gen); err != nil { return fmt.Errorf("updateGenerator chain %d at %s: %w", chainId.Uint64(), tableUpdaterAddr.Hex(), err) } } diff --git a/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go b/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go index 4bfda998..700d6cc4 100644 --- a/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go +++ b/test/integration/migration/avs_context_0_1_1_to_0_1_2_test.go @@ -8,7 +8,6 @@ import ( "gopkg.in/yaml.v3" ) -// assumes testNode(t, yamlStr) helper exists in this package like in your other test func TestMigration_0_1_1_to_0_1_2(t *testing.T) { oldYAML := ` version: 0.1.1 @@ -29,6 +28,11 @@ context: block: 31408197 url: "" block_time: 3 + transporter: + schedule: "0 */2 * * *" + private_key: "0x5f8e6420b9cb0c940e3d3f8b99177980785906d16fb3571f70d7a05ecf5f2172" + bls_private_key: "0x5f8e6420b9cb0c940e3d3f8b99177980785906d16fb3571f70d7a05ecf5f2172" + active_stake_roots: [] ` userNode := testNode(t, oldYAML) @@ -115,6 +119,24 @@ context: } }) + t.Run("transporter keys removed", func(t *testing.T) { + priv := migration.ResolveNode(migrated, []string{"context", "transporter", "private_key"}) + if priv != nil { + t.Errorf("expected context.transporter.private_key to be removed, got %v", priv) + } + bls := migration.ResolveNode(migrated, []string{"context", "transporter", "bls_private_key"}) + if bls != nil { + t.Errorf("expected context.transporter.bls_private_key to be removed, got %v", bls) + } + }) + + t.Run("transporter other fields preserved", func(t *testing.T) { + scheduleNode := migration.ResolveNode(migrated, []string{"context", "transporter", "schedule"}) + if scheduleNode == nil || scheduleNode.Value != "0 */2 * * *" { + t.Errorf("expected context.transporter.schedule preserved, got %v", scheduleNode) + } + }) + t.Run("other fields preserved", func(t *testing.T) { nameNode := migration.ResolveNode(migrated, []string{"context", "name"}) if nameNode == nil || nameNode.Value != "devnet" { From b015b782d753176aa5e22992327303e090b7d361 Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Wed, 10 Dec 2025 03:20:05 +0000 Subject: [PATCH 09/10] fix: remove transporter funding from FundWalletsDevnet --- pkg/common/devnet/funding.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pkg/common/devnet/funding.go b/pkg/common/devnet/funding.go index 26fa9992..4d844203 100644 --- a/pkg/common/devnet/funding.go +++ b/pkg/common/devnet/funding.go @@ -377,17 +377,6 @@ func FundWalletsDevnet(cfg *devkitcommon.ConfigWithContextConfig, rpcURL string, return err } - // Fund transporter - transporterPrivateKey := cfg.Context[DEVNET_CONTEXT].Transporter.PrivateKey - transporterECDSAKey, err := crypto.HexToECDSA(strings.TrimPrefix(transporterPrivateKey, "0x")) - if err != nil { - return fmt.Errorf("failed to parse private key: %w", err) - } - err = FundIfNeeded(ethClient, crypto.PubkeyToAddress(transporterECDSAKey.PublicKey), mnemonic) - if err != nil { - return err - } - return nil } From d333314a108d5653faa100c67cd1640c8de61e8f Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:14:30 +0000 Subject: [PATCH 10/10] fix: fund currOwner and AVS in transporter workflow --- pkg/commands/transporter.go | 46 ++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/pkg/commands/transporter.go b/pkg/commands/transporter.go index 6485edca..3b665dbe 100644 --- a/pkg/commands/transporter.go +++ b/pkg/commands/transporter.go @@ -316,7 +316,10 @@ func Transport(cCtx *cli.Context, initialRun bool) error { // On initial devnet Transport we take ownership of contracts and configure generator to use context keys if contextName == devnet.DEVNET_CONTEXT && initialRun { // Transfer ownership to our context configured PrivateKey - transferOwnership(logger, l1Config.RPCURL, crossChainRegistryAddress, privateKeyHex) + err := transferOwnership(logger, l1Config.RPCURL, mnemonic, crossChainRegistryAddress, privateKeyHex) + if err != nil { + return fmt.Errorf("failed to transfer ownership: %w", err) + } // Construct registry caller ccRegistryCaller, err := ICrossChainRegistry.NewICrossChainRegistryCaller(crossChainRegistryAddress, l1Client.RPCClient) @@ -348,7 +351,10 @@ func Transport(cCtx *cli.Context, initialRun bool) error { if chainId.Uint64() == uint64(l2ChainId) { rpcURL = l2Config.RPCURL } - transferOwnership(logger, rpcURL, tableUpdaterAddr, privateKeyHex) + err := transferOwnership(logger, rpcURL, mnemonic, tableUpdaterAddr, privateKeyHex) + if err != nil { + return fmt.Errorf("failed to transfer ownership: %w", err) + } // Read the current generator (avs,id) from OperatorTableUpdater gen, err := getGenerator(cCtx.Context, logger, cm, chainId, tableUpdaterAddr) @@ -412,7 +418,8 @@ func Transport(cCtx *cli.Context, initialRun bool) error { if err := configureCurveTypeAsAVS( cCtx.Context, logger, - l1RpcUrl, // KeyRegistrar is on L1 + l1RpcUrl, // KeyRegistrar is on L1, + mnemonic, ethcommon.HexToAddress(envCtx.EigenLayer.L1.KeyRegistrar), gen.Avs, uint32(gen.Id), @@ -929,13 +936,19 @@ func ScheduleTransportWithParserAndFunc(cCtx *cli.Context, cronExpr string, pars } // Impersonate the current owner and call *.transferOwnership(newOwner). -func transferOwnership(logger iface.Logger, rpcURL string, proxy ethcommon.Address, privateKey string) { +func transferOwnership(logger iface.Logger, rpcURL string, mnemonic string, proxy ethcommon.Address, privateKey string) error { ctx := context.Background() c, err := rpc.DialContext(ctx, rpcURL) if err != nil { logger.Error("failed to connect to rpc: %w", err) } + // Connect to an ethClient to fund AVS + ethClient, err := ethclient.Dial(rpcURL) + if err != nil { + return fmt.Errorf("failed to connect to L1 RPC: %w", err) + } + // Transporter private key - used to derive the new owner address priv := mustKey(logger, privateKey) newOwner := crypto.PubkeyToAddress(priv.PublicKey) @@ -950,6 +963,12 @@ func transferOwnership(logger iface.Logger, rpcURL string, proxy ethcommon.Addre currOwner := readOwner(ctx, logger, c, ownableABI, proxy) logger.Info("Current owner: %s", currOwner.Hex()) + // Fund the current owner to ensure it can submit a transferOwnership tx + err = devnet.FundIfNeeded(ethClient, currOwner, mnemonic) + if err != nil { + return fmt.Errorf("failed to fund %s", currOwner) + } + // Impersonate the current owner and fund it impersonate(ctx, logger, c, currOwner) defer stopImpersonate(ctx, c, currOwner) @@ -957,7 +976,7 @@ func transferOwnership(logger iface.Logger, rpcURL string, proxy ethcommon.Addre // Pack transferOwnership(newOwner) calldata, err := ownableABI.Pack("transferOwnership", newOwner) if err != nil { - logger.Error("failed to pack callData %w", err) + return fmt.Errorf("failed to pack callData: %w", err) } // Send tx via eth_sendTransaction from the impersonated owner to the proxy @@ -969,7 +988,7 @@ func transferOwnership(logger iface.Logger, rpcURL string, proxy ethcommon.Addre } var txHash ethcommon.Hash if err := c.CallContext(ctx, &txHash, "eth_sendTransaction", tx); err != nil { - logger.Error("failed to send tx: %w", err) + return fmt.Errorf("failed to send tx: %w", err) } // Await for tx receipt @@ -979,6 +998,8 @@ func transferOwnership(logger iface.Logger, rpcURL string, proxy ethcommon.Addre // Verify newOwnerRead := readOwner(ctx, logger, c, ownableABI, proxy) logger.Info("New owner: %s", newOwnerRead.Hex()) + + return nil } // Impersonate the AVS and call KeyRegistrar.configureOperatorSet(opSet, curveType) @@ -986,6 +1007,7 @@ func configureCurveTypeAsAVS( ctx context.Context, logger iface.Logger, rpcURL string, + mnemonic string, keyRegistrar ethcommon.Address, avs ethcommon.Address, opSetId uint32, @@ -997,6 +1019,12 @@ func configureCurveTypeAsAVS( return fmt.Errorf("rpc dial: %w", err) } + // Connect to an ethClient to fund AVS + ethClient, err := ethclient.Dial(rpcURL) + if err != nil { + return fmt.Errorf("failed to connect to L1 RPC: %w", err) + } + // Build minimal ABI krABI := mustABI(logger, `[ {"inputs":[{"components":[{"internalType":"address","name":"avs","type":"address"},{"internalType":"uint32","name":"id","type":"uint32"}],"internalType":"struct OperatorSet","name":"opSet","type":"tuple"}],"name":"getOperatorSetCurveType","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"}, @@ -1010,6 +1038,12 @@ func configureCurveTypeAsAVS( } opSet := opSetT{Avs: avs, Id: opSetId} + // Fund the AVS to ensure it can submit configureOperatorSet tx + err = devnet.FundIfNeeded(ethClient, avs, mnemonic) + if err != nil { + return fmt.Errorf("failed to fund %s", avs) + } + // Read current curve type; skip if already set calldataGet, _ := krABI.Pack("getOperatorSetCurveType", opSet) var out string