diff --git a/README.md b/README.md index 582ab829..c1490dac 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,16 @@ curl -fsSL https://raw.githubusercontent.com/Layr-Labs/devkit-cli/main/install-d Download the binary for your platform: ```bash # macOS (Apple Silicon) -mkdir -p $HOME/bin && curl -sL https://s3.amazonaws.com/eigenlayer-devkit-releases/v0.0.10/devkit-darwin-arm64-v0.0.10.tar.gz | tar xz -C "$HOME/bin" +mkdir -p $HOME/bin && curl -sL https://s3.amazonaws.com/eigenlayer-devkit-releases/v0.1.0-preview.2.rc/devkit-darwin-arm64-v0.1.0-preview.2.rc.tar.gz | tar xz -C "$HOME/bin" # macOS (Intel) -mkdir -p $HOME/bin && curl -sL https://s3.amazonaws.com/eigenlayer-devkit-releases/v0.0.10/devkit-darwin-amd64-v0.0.10.tar.gz | tar xz -C "$HOME/bin" +mkdir -p $HOME/bin && curl -sL https://s3.amazonaws.com/eigenlayer-devkit-releases/v0.1.0-preview.2.rc/devkit-darwin-amd64-v0.1.0-preview.2.rc.tar.gz | tar xz -C "$HOME/bin" # Linux (x86_64 / AMD64) -mkdir -p $HOME/bin && curl -sL https://s3.amazonaws.com/eigenlayer-devkit-releases/v0.0.10/devkit-linux-amd64-v0.0.10.tar.gz | tar xz -C "$HOME/bin" +mkdir -p $HOME/bin && curl -sL https://s3.amazonaws.com/eigenlayer-devkit-releases/v0.1.0-preview.2.rc/devkit-linux-amd64-v0.1.0-preview.2.rc.tar.gz | tar xz -C "$HOME/bin" # Linux (ARM64 / aarch64) -mkdir -p $HOME/bin && curl -sL https://s3.amazonaws.com/eigenlayer-devkit-releases/v0.0.10/devkit-linux-arm64-v0.0.10.tar.gz | tar xz -C "$HOME/bin" +mkdir -p $HOME/bin && curl -sL https://s3.amazonaws.com/eigenlayer-devkit-releases/v0.1.0-preview.2.rc/devkit-linux-arm64-v0.1.0-preview.2.rc.tar.gz | tar xz -C "$HOME/bin" ``` Add to your PATH: @@ -521,6 +521,60 @@ devkit avs build --verbose ``` --- + +## Deploying to Testnet (v0.1.0+) + +As of **v0.1.0**, DevKit supports deploying AVS contracts to public testnets. This is the next step after local development and testing. + +### Create a Testnet Context + +You must create a separate context for your testnet deployment: + +```bash +devkit avs context create --context testnet +``` + +Set it as the active context: + +```bash +devkit avs config --set project.context="testnet" +``` + +Edit the testnet configuration to set RPC endpoints, keys, and other environment details: + +```bash +devkit avs context --edit --context testnet +``` + +> **Tip:** +> Use reliable archive RPC endpoints for both L1 and L2 chains. Configure private keys for deployment wallets in the testnet context file or via `.env` for security. + +--- + +### Deploy AVS Contracts to Testnet + +Once the `testnet` context is configured and active, you can deploy to each chain: + +- **Deploy L1 contracts**: +```bash +devkit avs deploy contracts l1 +``` + +- **Deploy L2 contracts**: +```bash +devkit avs deploy contracts l2 +``` + +> Both commands will use the RPC URLs and keys from your active context. + +--- + +### Next Steps After Deployment +- Verify contract addresses in your testnet context file. +- Register operators and run your AVS offchain services pointing to the testnet. +- Optionally, publish a release for operators using `devkit avs release publish`. + + ## Upgrade Process diff --git a/VERSION b/VERSION index 51bbb66d..d7565e01 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.0.10 +v0.1.0-preview.2.rc diff --git a/config/contexts/migrations/v0.0.8-v0.0.9.go b/config/contexts/migrations/v0.0.8-v0.0.9.go index 3a1daf51..16e0b909 100644 --- a/config/contexts/migrations/v0.0.8-v0.0.9.go +++ b/config/contexts/migrations/v0.0.8-v0.0.9.go @@ -100,14 +100,6 @@ func Migration_0_0_8_to_0_0_9(user, old, new *yaml.Node) (*yaml.Node, error) { return &yaml.Node{Kind: yaml.ScalarNode, Value: "0xaCB5DE6aa94a1908E6FA577C2ade65065333B450"} }, }, - // Add skip-setup flag to avs config - { - Path: []string{"context", "avs", "skip_setup"}, - Condition: migration.Always{}, - Transform: func(_ *yaml.Node) *yaml.Node { - return &yaml.Node{Kind: yaml.ScalarNode, Value: "false"} - }, - }, }, } diff --git a/config/contexts/migrations/v0.0.9-v0.1.0.go b/config/contexts/migrations/v0.0.9-v0.1.0.go new file mode 100644 index 00000000..6bf57723 --- /dev/null +++ b/config/contexts/migrations/v0.0.9-v0.1.0.go @@ -0,0 +1,123 @@ +package contextMigrations + +import ( + "fmt" + + "github.com/Layr-Labs/devkit-cli/pkg/migration" + + "gopkg.in/yaml.v3" +) + +func Migration_0_0_9_to_0_1_0(user, old, new *yaml.Node) (*yaml.Node, error) { + engine := migration.PatchEngine{ + Old: old, + New: new, + User: user, + Rules: []migration.PatchRule{ + // Move Operators keystore to map + { + Path: []string{"context", "operators"}, + Condition: migration.Always{}, + Transform: func(_ *yaml.Node) *yaml.Node { + avsNode := migration.ResolveNode(user, []string{"context", "avs", "address"}) + avsAddr := "" + if avsNode != nil { + avsAddr = avsNode.Value + } + + ops := migration.ResolveNode(user, []string{"context", "operators"}) + if ops == nil || ops.Kind != yaml.SequenceNode { + return ops + } + out := migration.CloneNode(ops) + + // Fixed operatorSet IDs + opsetIDs := []int{0, 1} + + for _, op := range out.Content { + if op.Kind != yaml.MappingNode { + continue + } + + ecdsaPath := "" + if n := migration.ResolveNode(op, []string{"ecdsa_keystore_path"}); n != nil { + ecdsaPath = n.Value + } + ecdsaPass := "" + if n := migration.ResolveNode(op, []string{"ecdsa_keystore_password"}); n != nil { + ecdsaPass = n.Value + } + blsPath := "" + if n := migration.ResolveNode(op, []string{"bls_keystore_path"}); n != nil { + blsPath = n.Value + } + blsPass := "" + if n := migration.ResolveNode(op, []string{"bls_keystore_password"}); n != nil { + blsPass = n.Value + } + + // build keystores + keystoresSeq := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"} + + for _, setID := range opsetIDs { + ksMap := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + + ksMap.Content = append(ksMap.Content, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "avs"}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: avsAddr}, + + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "operatorSet"}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: fmt.Sprintf("%d", setID)}, + + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "ecdsa_keystore_path"}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: ecdsaPath}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "ecdsa_keystore_password"}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: ecdsaPass}, + + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "bls_keystore_path"}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: blsPath}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "bls_keystore_password"}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: blsPass}, + ) + + keystoresSeq.Content = append(keystoresSeq.Content, ksMap) + } + + // drop old flat fields and any preexisting keystore/keystores + c := op.Content[:0] + for j := 0; j < len(op.Content); j += 2 { + k := op.Content[j].Value + if k == "ecdsa_keystore_path" || + k == "ecdsa_keystore_password" || + k == "bls_keystore_path" || + k == "bls_keystore_password" || + k == "keystores" { + continue + } + c = append(c, op.Content[j], op.Content[j+1]) + } + op.Content = c + + // write new keystores + op.Content = append(op.Content, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "keystores"}, + keystoresSeq, + ) + } + return out + }, + }, + }, + } + + 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.0" + } + + return user, nil +} diff --git a/config/contexts/registry.go b/config/contexts/registry.go index eb18f773..0799e8e6 100644 --- a/config/contexts/registry.go +++ b/config/contexts/registry.go @@ -15,7 +15,7 @@ import ( ) // Set the latest version -const LatestVersion = "0.0.9" +const LatestVersion = "0.1.0" // Array of default contexts to create in project var DefaultContexts = [...]string{ @@ -53,6 +53,9 @@ var v0_0_8_default []byte //go:embed v0.0.9.yaml var v0_0_9_default []byte +//go:embed v0.1.0.yaml +var v0_1_0_default []byte + // Map of context name -> content var ContextYamls = map[string][]byte{ "0.0.1": v0_0_1_default, @@ -64,6 +67,7 @@ var ContextYamls = map[string][]byte{ "0.0.7": v0_0_7_default, "0.0.8": v0_0_8_default, "0.0.9": v0_0_9_default, + "0.1.0": v0_1_0_default, } // Map of sequential migrations @@ -124,6 +128,13 @@ var MigrationChain = []migration.MigrationStep{ OldYAML: v0_0_8_default, NewYAML: v0_0_9_default, }, + { + From: "0.0.9", + To: "0.1.0", + Apply: contextMigrations.Migration_0_0_9_to_0_1_0, + OldYAML: v0_0_9_default, + NewYAML: v0_1_0_default, + }, } func MigrateContexts(logger iface.Logger) (int, error) { diff --git a/config/contexts/v0.0.9.yaml b/config/contexts/v0.0.9.yaml index fdef7217..0b48943f 100644 --- a/config/contexts/v0.0.9.yaml +++ b/config/contexts/v0.0.9.yaml @@ -99,7 +99,6 @@ context: bls_keystore_password: "testpass" # AVS configuration avs: - skip_setup: false address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" avs_private_key: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" # Anvil Private Key 1 metadata_url: "https://my-org.com/avs/metadata.json" diff --git a/config/contexts/v0.1.0.yaml b/config/contexts/v0.1.0.yaml new file mode 100644 index 00000000..7bf50209 --- /dev/null +++ b/config/contexts/v0.1.0.yaml @@ -0,0 +1,183 @@ +# Devnet context to be used for local deployments against Anvil chain +version: 0.1.0 +context: + # Name of the context + name: "devnet" + # Chains available to this context + chains: + l1: + chain_id: 31337 + rpc_url: "http://localhost:8545" + fork: + block: 8836193 + url: "" + block_time: 3 + l2: + chain_id: 31338 + rpc_url: "http://localhost:9545" + fork: + block: 28820370 + url: "" + block_time: 3 + # 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" + 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/config/templates.yaml b/config/templates.yaml index b87306c7..d0679605 100644 --- a/config/templates.yaml +++ b/config/templates.yaml @@ -3,7 +3,7 @@ architectures: languages: go: baseUrl: "https://github.com/Layr-Labs/hourglass-avs-template" - version: "v0.0.17" + version: "v0.0.18" ts: baseUrl: "" version: "" diff --git a/pkg/commands/deploy_actions.go b/pkg/commands/deploy_actions.go index 0bae021f..a08e53ac 100644 --- a/pkg/commands/deploy_actions.go +++ b/pkg/commands/deploy_actions.go @@ -161,7 +161,7 @@ func StartDeployL1Action(cCtx *cli.Context) error { // Register AVS with EigenLayer logger.Title("Registering AVS with EigenLayer...") - if !(cCtx.Bool("skip-setup") || envCtx.Avs.SkipSetup) { + if !cCtx.Bool("skip-setup") { if err := UpdateAVSMetadataAction(cCtx, logger); err != nil { return fmt.Errorf("updating AVS metadata failed: %w", err) } @@ -976,6 +976,8 @@ func ConfigureOpSetCurveTypeAction(cCtx *cli.Context, logger iface.Logger) error logger.Info("Configuring curve type %s for operator set %d", opSet.CurveType, opSet.OperatorSetID) + // Check current curveType - throw if we are attempting to change it + // Configure the curve type err = contractCaller.ConfigureOpSetCurveType( cCtx.Context, avsAddress, @@ -1130,10 +1132,24 @@ func RegisterKeyInKeyRegistrarAction(cCtx *cli.Context, logger iface.Logger) err if err != nil { return fmt.Errorf("failed to create contract caller: %w", err) } - blskeystorePath := operator.BlsKeystorePath - blskeystorePassword := operator.BlsKeystorePassword - keystoreData, err := keystore.LoadKeystoreFile(blskeystorePath) + var blskeystorePath, blskeystorePassword string + for _, ks := range operator.Keystores { + if ks.OperatorSet == op.OperatorSetID { + blskeystorePath = ks.BlsKeystorePath + blskeystorePassword = ks.BlsKeystorePassword + break + } + } + + if blskeystorePath == "" { + return fmt.Errorf("no bls keystore found for OperatorSet %d", op.OperatorSetID) + } + + keystoreData, err := keystore.LoadKeystoreFile(blskeystorePath) + if err != nil { + return fmt.Errorf("failed to load keystore %q: %w", blskeystorePath, err) + } if err != nil { return fmt.Errorf("failed to load the keystore file from given path %s error %w", blskeystorePath, err) } diff --git a/pkg/commands/devnet_actions.go b/pkg/commands/devnet_actions.go index cd9cca4c..09afe584 100644 --- a/pkg/commands/devnet_actions.go +++ b/pkg/commands/devnet_actions.go @@ -338,7 +338,7 @@ func StartDevnetAction(cCtx *cli.Context) error { time.Sleep(1 * time.Second) logger.Title("Registering AVS with EigenLayer...") - if !(cCtx.Bool("skip-setup") || envCtx.Avs.SkipSetup) { + if !cCtx.Bool("skip-setup") { if err := UpdateAVSMetadataAction(cCtx, logger); err != nil { return fmt.Errorf("updating AVS metadata failed: %w", err) } @@ -1472,14 +1472,14 @@ func stopBothContainersByPort(cCtx *cli.Context, log iface.Logger, targetPort in // loadOperatorECDSAKey loads an operator's ECDSA private key from keystore or plaintext func loadOperatorECDSAKey(operator common.OperatorSpec) (string, error) { // Check if ECDSA keystore is configured - if operator.ECDSAKeystorePath != "" && operator.ECDSAKeystorePassword != "" { + if len(operator.Keystores) > 0 && operator.Keystores[0].ECDSAKeystorePath != "" && operator.Keystores[0].ECDSAKeystorePassword != "" { // Load from keystore - keystoreData, err := os.ReadFile(operator.ECDSAKeystorePath) + keystoreData, err := os.ReadFile(operator.Keystores[0].ECDSAKeystorePath) if err != nil { - return "", fmt.Errorf("failed to read ECDSA keystore file %s: %w", operator.ECDSAKeystorePath, err) + return "", fmt.Errorf("failed to read ECDSA keystore file %s: %w", operator.Keystores[0].ECDSAKeystorePath, err) } - key, err := ethkeystore.DecryptKey(keystoreData, operator.ECDSAKeystorePassword) + key, err := ethkeystore.DecryptKey(keystoreData, operator.Keystores[0].ECDSAKeystorePassword) if err != nil { return "", fmt.Errorf("failed to decrypt ECDSA keystore: %w", err) } diff --git a/pkg/commands/devnet_actions_test.go b/pkg/commands/devnet_actions_test.go index 12101542..b1793d05 100644 --- a/pkg/commands/devnet_actions_test.go +++ b/pkg/commands/devnet_actions_test.go @@ -19,10 +19,14 @@ func TestLoadECDSAKeysFromKeystores(t *testing.T) { // No keystore - use plaintext }, { - Address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", - ECDSAKey: "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", - ECDSAKeystorePath: "keystores/nonexistent.ecdsa.keystore.json", - ECDSAKeystorePassword: "testpass", + Address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", + ECDSAKey: "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", + Keystores: []common.OperatorKeystores{ + { + ECDSAKeystorePath: "keystores/nonexistent.ecdsa.keystore.json", + ECDSAKeystorePassword: "testpass", + }, + }, }, { Address: "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", @@ -65,10 +69,14 @@ func TestLoadECDSAKeysFromKeystores(t *testing.T) { t.Run("use plaintext key when keystore path without password", func(t *testing.T) { // Test operator with keystore path but no password - should use plaintext opWithPath := common.OperatorSpec{ - Address: "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955", - ECDSAKey: "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356", - ECDSAKeystorePath: "keystores/operator5.ecdsa.keystore.json", - // No password provided + Address: "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955", + ECDSAKey: "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356", + Keystores: []common.OperatorKeystores{ + { + ECDSAKeystorePath: "keystores/operator5.ecdsa.keystore.json", + // No password provided + }, + }, } key, err := loadOperatorECDSAKey(opWithPath) require.NoError(t, err) @@ -99,18 +107,27 @@ func TestMixedBLSAndECDSAOperators(t *testing.T) { // Test mixed operator configuration operators := []common.OperatorSpec{ { - Address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906", - ECDSAKey: "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", - ECDSAKeystorePath: "keystores/operator1.ecdsa.keystore.json", - ECDSAKeystorePassword: "testpass", - BlsKeystorePath: "keystores/operator1.bls.keystore.json", - BlsKeystorePassword: "testpass", + Address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906", + ECDSAKey: "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", + Keystores: []common.OperatorKeystores{ + { + ECDSAKeystorePath: "keystores/operator1.ecdsa.keystore.json", + ECDSAKeystorePassword: "testpass", + BlsKeystorePath: "keystores/operator1.bls.keystore.json", + BlsKeystorePassword: "testpass", + }, + }, }, { - Address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", - ECDSAKey: "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", - BlsKeystorePath: "keystores/operator2.bls.keystore.json", - BlsKeystorePassword: "testpass", + Address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", + ECDSAKey: "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", + Keystores: []common.OperatorKeystores{ + { + BlsKeystorePath: "keystores/operator2.bls.keystore.json", + BlsKeystorePassword: "testpass", + }, + }, + // No ECDSA keystore - uses plaintext }, } diff --git a/pkg/common/config.go b/pkg/common/config.go index 40ca1612..8e8cc2e4 100644 --- a/pkg/common/config.go +++ b/pkg/common/config.go @@ -35,14 +35,21 @@ type ForkConfig struct { } type OperatorSpec struct { - Address string `json:"address" yaml:"address"` - ECDSAKey string `json:"ecdsa_key,omitempty" yaml:"ecdsa_key,omitempty"` - ECDSAKeystorePath string `json:"ecdsa_keystore_path,omitempty" yaml:"ecdsa_keystore_path,omitempty"` - ECDSAKeystorePassword string `json:"ecdsa_keystore_password,omitempty" yaml:"ecdsa_keystore_password,omitempty"` - BlsKeystorePath string `json:"bls_keystore_path" yaml:"bls_keystore_path"` - BlsKeystorePassword string `json:"bls_keystore_password" yaml:"bls_keystore_password"` - Stake string `json:"stake,omitempty" yaml:"stake,omitempty"` - Allocations []OperatorAllocation `json:"allocations,omitempty" yaml:"allocations,omitempty"` + Address string `json:"address" yaml:"address"` + ECDSAKey string `json:"ecdsa_key,omitempty" yaml:"ecdsa_key,omitempty"` + Stake string `json:"stake,omitempty" yaml:"stake,omitempty"` + Keystores []OperatorKeystores `json:"keystores,omitempty" yaml:"keystores,omitempty"` + Allocations []OperatorAllocation `json:"allocations,omitempty" yaml:"allocations,omitempty"` +} + +// OperatorKeystore defines keystore data available to an operator +type OperatorKeystores struct { + AVSAddress string `json:"avs,omitempty" yaml:"avs,omitempty"` + OperatorSet uint64 `json:"operatorSet,omitempty" yaml:"operatorSet,omitempty"` + ECDSAKeystorePath string `json:"ecdsa_keystore_path,omitempty" yaml:"ecdsa_keystore_path,omitempty"` + ECDSAKeystorePassword string `json:"ecdsa_keystore_password,omitempty" yaml:"ecdsa_keystore_password,omitempty"` + BlsKeystorePath string `json:"bls_keystore_path" yaml:"bls_keystore_path"` + BlsKeystorePassword string `json:"bls_keystore_password" yaml:"bls_keystore_password"` } // OperatorAllocation defines strategy allocation for an operator @@ -74,7 +81,6 @@ type StakerDeposits struct { } type AvsConfig struct { - SkipSetup bool `json:"skip_setup" yaml:"skip_setup"` Address string `json:"address" yaml:"address"` MetadataUri string `json:"metadata_url" yaml:"metadata_url"` AVSPrivateKey string `json:"avs_private_key" yaml:"avs_private_key"` @@ -170,7 +176,6 @@ type StakeRootEntry struct { } type Transporter struct { - SkipTransporter bool `json:"skip_transporter" yaml:"skip_transporter"` Schedule string `json:"schedule" yaml:"schedule"` PrivateKey string `json:"private_key" yaml:"private_key"` BlsPrivateKey string `json:"bls_private_key" yaml:"bls_private_key"` diff --git a/pkg/common/config_test.go b/pkg/common/config_test.go index dd74da60..db3e37ec 100644 --- a/pkg/common/config_test.go +++ b/pkg/common/config_test.go @@ -42,10 +42,10 @@ func TestLoadConfigWithContextConfig_FromCopiedTempFile(t *testing.T) { assert.Equal(t, "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", cfg.Context["devnet"].DeployerPrivateKey) assert.Equal(t, "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", cfg.Context["devnet"].AppDeployerPrivateKey) - assert.Equal(t, "keystores/operator1.bls.keystore.json", cfg.Context["devnet"].Operators[0].BlsKeystorePath) - assert.Equal(t, "keystores/operator2.bls.keystore.json", cfg.Context["devnet"].Operators[1].BlsKeystorePath) - assert.Equal(t, "testpass", cfg.Context["devnet"].Operators[0].BlsKeystorePassword) - assert.Equal(t, "testpass", cfg.Context["devnet"].Operators[0].BlsKeystorePassword) + assert.Equal(t, "keystores/operator1.bls.keystore.json", cfg.Context["devnet"].Operators[0].Keystores[0].BlsKeystorePath) + assert.Equal(t, "keystores/operator2.bls.keystore.json", cfg.Context["devnet"].Operators[1].Keystores[0].BlsKeystorePath) + assert.Equal(t, "testpass", cfg.Context["devnet"].Operators[0].Keystores[0].BlsKeystorePassword) + assert.Equal(t, "testpass", cfg.Context["devnet"].Operators[0].Keystores[0].BlsKeystorePassword) // In v0.0.6, operators use allocations instead of stake assert.NotEmpty(t, cfg.Context["devnet"].Operators[0].Allocations) diff --git a/pkg/common/contract_caller.go b/pkg/common/contract_caller.go index 62b26fc6..fe969ec1 100644 --- a/pkg/common/contract_caller.go +++ b/pkg/common/contract_caller.go @@ -199,7 +199,7 @@ func (cc *ContractCaller) CreateOperatorSets(ctx context.Context, avsAddress com } err = cc.SendAndWaitForTransaction(ctx, "CreateOperatorSets", func() (*types.Transaction, error) { - tx, err := allocationManager.CreateOperatorSets(opts, avsAddress, createSetParams) + tx, err := allocationManager.CreateOperatorSets(opts, avsAddress, filteredParams) if err == nil && tx != nil { cc.logger.Debug( "Transaction hash for CreateOperatorSets: %s\n"+ @@ -227,6 +227,16 @@ func (cc *ContractCaller) RegisterAsOperator(ctx context.Context, operatorAddres return fmt.Errorf("failed to get DelegationManager: %w", err) } + exists, err := delegationManager.IsOperator(nil, operatorAddress) + if err != nil { + return fmt.Errorf("failed to check operator exists %d: %w", operatorAddress, err) + } + + if exists { + cc.logger.Info("Operator '%s' already registered, skipping", operatorAddress) + return nil + } + err = cc.SendAndWaitForTransaction(ctx, fmt.Sprintf("RegisterAsOperator for %s", operatorAddress.Hex()), func() (*types.Transaction, error) { tx, err := delegationManager.RegisterAsOperator(opts, operatorAddress, allocationDelay, metadataURI) if err == nil && tx != nil { @@ -743,7 +753,19 @@ func (cc *ContractCaller) RegisterKeyInKeyRegistrar(ctx context.Context, operato if err != nil { return fmt.Errorf("signature not in correct subgroup: %w", err) } + operatorSet := keyregistrar.OperatorSet{Avs: avsAddress, Id: opSetId} + + exists, err := keyRegistrar.IsRegistered(nil, operatorSet, operatorAddress) + if err != nil { + return fmt.Errorf("failed to check operator (%d) is registered: %w", operatorAddress, err) + } + + if exists { + cc.logger.Info("Operator '%s' already registered for operatorSet '%d' and AVS '%s', skipping", operatorAddress, opSetId, avsAddress) + return nil + } + err = cc.SendAndWaitForTransaction(ctx, "RegisterKeyInKeyRegistrar", func() (*types.Transaction, error) { tx, err := keyRegistrar.RegisterKey(opts, operatorAddress, operatorSet, keyData, g1Bytes) return tx, err diff --git a/pkg/common/devnet/funding.go b/pkg/common/devnet/funding.go index 36169d70..f97d6947 100644 --- a/pkg/common/devnet/funding.go +++ b/pkg/common/devnet/funding.go @@ -307,14 +307,14 @@ func FundWalletsDevnet(cfg *devkitcommon.ConfigWithContextConfig, rpcURL string) var privateKey *ecdsa.PrivateKey // Check if ECDSA keystore is configured - if operator.ECDSAKeystorePath != "" && operator.ECDSAKeystorePassword != "" { + if len(operator.Keystores) > 0 && operator.Keystores[0].ECDSAKeystorePath != "" && operator.Keystores[0].ECDSAKeystorePassword != "" { // Load from keystore - keystoreData, err := os.ReadFile(operator.ECDSAKeystorePath) + keystoreData, err := os.ReadFile(operator.Keystores[0].ECDSAKeystorePath) if err != nil { - log.Fatalf("failed to read ECDSA keystore file %s: %v", operator.ECDSAKeystorePath, err) + log.Fatalf("failed to read ECDSA keystore file %s: %v", operator.Keystores[0].ECDSAKeystorePath, err) } - key, err := keystore.DecryptKey(keystoreData, operator.ECDSAKeystorePassword) + key, err := keystore.DecryptKey(keystoreData, operator.Keystores[0].ECDSAKeystorePassword) if err != nil { log.Fatalf("failed to decrypt ECDSA keystore: %v", err) } diff --git a/pkg/template/config_test.go b/pkg/template/config_test.go index bb244516..e80b87da 100644 --- a/pkg/template/config_test.go +++ b/pkg/template/config_test.go @@ -17,7 +17,7 @@ func TestLoadConfig(t *testing.T) { } expectedBaseURL := "https://github.com/Layr-Labs/hourglass-avs-template" - expectedVersion := "v0.0.17" + expectedVersion := "v0.0.18" if mainBaseURL != expectedBaseURL { t.Errorf("Unexpected main template base URL: got %s, want %s", mainBaseURL, expectedBaseURL) diff --git a/test/integration/migration/avs_context_migration_test.go b/test/integration/migration/avs_context_migration_test.go index b0cdea2b..eca0a7cf 100644 --- a/test/integration/migration/avs_context_migration_test.go +++ b/test/integration/migration/avs_context_migration_test.go @@ -1063,6 +1063,297 @@ context: }) } +func TestAVSContextMigration_0_0_8_to_0_0_9_PatchesAndAddsSkipSetup(t *testing.T) { + // v0.0.8 input with existing structure and old values + const in = `version: 0.0.8 +context: + name: "pre-009" + chains: + l1: + fork: + block: "11111111" + l2: + fork: + block: "22222222" + eigenlayer: + l1: + cross_chain_registry: "0xOLD_L1_C_C_R" + operator_table_updater: "0xOLD_L1_O_T_U" + key_registrar: "0xOLD_L1_K_R" + bn254_table_calculator: "0xOLD_L1_BN254_T_C" + ecdsa_table_calculator: "0xOLD_L1_ECDSA_T_C" + l2: + task_mailbox: "0xOLD_L2_T_M" + operator_table_updater: "0xOLD_L2_O_T_U" + bn254_certificate_verifier: "0xOLD_L2_BN254_C_V" + ecdsa_certificate_verifier: "0xOLD_L2_ECDSA_C_V" + avs: + address: "0xAVS" +` + + userNode := testNode(t, in) + + // find the step 0.0.8 -> 0.0.9 + var step migration.MigrationStep + for _, s := range contexts.MigrationChain { + if s.From == "0.0.8" && s.To == "0.0.9" { + step = s + break + } + } + if step.Apply == nil { + t.Fatalf("migration step 0.0.8 -> 0.0.9 not found") + } + + out, err := migration.MigrateNode(userNode, "0.0.8", "0.0.9", []migration.MigrationStep{step}) + if err != nil { + t.Fatalf("migrate: %v", err) + } + + // print migrated YAML for debugging + if b, err := yaml.Marshal(out); err == nil { + t.Log("\n" + string(b)) + } + + t.Run("version bumped", func(t *testing.T) { + v := migration.ResolveNode(out, []string{"version"}) + if v == nil || v.Value != "0.0.9" { + t.Errorf("want version 0.0.9, got %v", v) + } + }) + + t.Run("fork blocks updated", func(t *testing.T) { + l1 := migration.ResolveNode(out, []string{"context", "chains", "l1", "fork", "block"}) + l2 := migration.ResolveNode(out, []string{"context", "chains", "l2", "fork", "block"}) + if l1 == nil || l1.Value != "8836193" { + t.Errorf("l1.fork.block want 8836193, got %v", l1) + } + if l2 == nil || l2.Value != "28820370" { + t.Errorf("l2.fork.block want 28820370, got %v", l2) + } + }) + + t.Run("L1 addresses updated", func(t *testing.T) { + C_C_R := migration.ResolveNode(out, []string{"context", "eigenlayer", "l1", "cross_chain_registry"}) + O_T_U := migration.ResolveNode(out, []string{"context", "eigenlayer", "l1", "operator_table_updater"}) + K_R := migration.ResolveNode(out, []string{"context", "eigenlayer", "l1", "key_registrar"}) + B_N := migration.ResolveNode(out, []string{"context", "eigenlayer", "l1", "bn254_table_calculator"}) + E_C := migration.ResolveNode(out, []string{"context", "eigenlayer", "l1", "ecdsa_table_calculator"}) + + if C_C_R == nil || C_C_R.Value != "0x287381B1570d9048c4B4C7EC94d21dDb8Aa1352a" { + t.Errorf("l1.cross_chain_registry updated wrong, got %v", C_C_R) + } + if O_T_U == nil || O_T_U.Value != "0xB02A15c6Bd0882b35e9936A9579f35FB26E11476" { + t.Errorf("l1.operator_table_updater updated wrong, got %v", O_T_U) + } + if K_R == nil || K_R.Value != "0xA4dB30D08d8bbcA00D40600bee9F029984dB162a" { + t.Errorf("l1.key_registrar updated wrong, got %v", K_R) + } + if B_N == nil || B_N.Value != "0xa19E3B00cf4aC46B5e6dc0Bbb0Fb0c86D0D65603" { + t.Errorf("l1.bn254_table_calculator updated wrong, got %v", B_N) + } + if E_C == nil || E_C.Value != "0xaCB5DE6aa94a1908E6FA577C2ade65065333B450" { + t.Errorf("l1.ecdsa_table_calculator updated wrong, got %v", E_C) + } + }) + + t.Run("L2 addresses updated", func(t *testing.T) { + T_M := migration.ResolveNode(out, []string{"context", "eigenlayer", "l2", "task_mailbox"}) + O_T_U := migration.ResolveNode(out, []string{"context", "eigenlayer", "l2", "operator_table_updater"}) + B_N := migration.ResolveNode(out, []string{"context", "eigenlayer", "l2", "bn254_certificate_verifier"}) + E_C := migration.ResolveNode(out, []string{"context", "eigenlayer", "l2", "ecdsa_certificate_verifier"}) + + if T_M == nil || T_M.Value != "0xB99CC53e8db7018f557606C2a5B066527bF96b26" { + t.Errorf("l2.task_mailbox updated wrong, got %v", T_M) + } + if O_T_U == nil || O_T_U.Value != "0xB02A15c6Bd0882b35e9936A9579f35FB26E11476" { + t.Errorf("l2.operator_table_updater updated wrong, got %v", O_T_U) + } + if B_N == nil || B_N.Value != "0xff58A373c18268F483C1F5cA03Cf885c0C43373a" { + t.Errorf("l2.bn254_certificate_verifier updated wrong, got %v", B_N) + } + if E_C == nil || E_C.Value != "0xb3Cd1A457dEa9A9A6F6406c6419B1c326670A96F" { + t.Errorf("l2.ecdsa_certificate_verifier updated wrong, got %v", E_C) + } + }) + + t.Run("unrelated fields preserved", func(t *testing.T) { + name := migration.ResolveNode(out, []string{"context", "name"}) + if name == nil || name.Value != "pre-009" { + t.Errorf("context.name mutated, got %v", name) + } + }) +} + +func TestAVSContextMigration_0_0_9_to_0_1_0_FixedOpSets(t *testing.T) { + // v0.0.9 input with flat keystore fields + customYAML := `version: 0.0.9 +context: + name: "custom-context" + avs: + address: "0xAVS_ADDR" + operators: + - address: "0xOP1" + ecdsa_key: "0xECDSAKEY1" + ecdsa_keystore_path: "keystores/operator1.ecdsa.keystore.json" + ecdsa_keystore_password: "pass1" + bls_keystore_path: "keystores/operator1.bls.keystore.json" + bls_keystore_password: "bpass1" + custom_field: "keepme1" + - address: "0xOP2" + ecdsa_key: "0xECDSAKEY2" + ecdsa_keystore_path: "keystores/operator2.ecdsa.keystore.json" + ecdsa_keystore_password: "pass2" + bls_keystore_path: "keystores/operator2.bls.keystore.json" + bls_keystore_password: "bpass2" + allocations: + - strategy_address: "0xSTRAT" + name: "stETH_Strategy" + operator_set_allocations: + - operator_set: "0" + allocation_in_wads: "500000000000000000" + - operator_set: "1" + allocation_in_wads: "500000000000000000" +` + + userNode := testNode(t, customYAML) + + // locate the 0.0.9 -> 0.1.0 step from the chain + var step migration.MigrationStep + for _, s := range contexts.MigrationChain { + if s.From == "0.0.9" && s.To == "0.1.0" { + step = s + break + } + } + if step.Apply == nil { + t.Fatalf("migration step 0.0.9 -> 0.1.0 not found") + } + + migrated, err := migration.MigrateNode(userNode, "0.0.9", "0.1.0", []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.0" { + t.Errorf("expected version 0.1.0, got %v", v) + } + }) + + t.Run("operator 1 keystores created and fields preserved", func(t *testing.T) { + op := migration.ResolveNode(migrated, []string{"context", "operators", "0"}) + if op == nil { + t.Fatal("operator[0] missing") + } + + // preserved fields + if got := migration.ResolveNode(op, []string{"address"}); got == nil || got.Value != "0xOP1" { + t.Errorf("address not preserved, got %v", got) + } + if got := migration.ResolveNode(op, []string{"ecdsa_key"}); got == nil || got.Value != "0xECDSAKEY1" { + t.Errorf("ecdsa_key not preserved, got %v", got) + } + if got := migration.ResolveNode(op, []string{"custom_field"}); got == nil || got.Value != "keepme1" { + t.Errorf("custom_field not preserved, got %v", got) + } + + // old flat fields removed + for _, k := range []string{"ecdsa_keystore_path", "ecdsa_keystore_password", "bls_keystore_path", "bls_keystore_password", "keystore"} { + if n := migration.ResolveNode(op, []string{k}); n != nil { + t.Errorf("expected %s to be removed, found %v", k, n) + } + } + + // new keystores + ks := migration.ResolveNode(op, []string{"keystores"}) + if ks == nil || ks.Kind != 2 /* yaml.SequenceNode */ || len(ks.Content) != 2 { + t.Fatalf("expected keystores sequence with 2 entries, got %#v", ks) + } + + // entry 1: operatorSet 0 + ks0 := ks.Content[0] + check := func(path []string, want string) { + n := migration.ResolveNode(ks0, path) + if n == nil || n.Value != want { + t.Errorf("expected %v == %q, got %v", path, want, n) + } + } + check([]string{"avs"}, "0xAVS_ADDR") + check([]string{"operatorSet"}, "0") + check([]string{"ecdsa_keystore_path"}, "keystores/operator1.ecdsa.keystore.json") + check([]string{"ecdsa_keystore_password"}, "pass1") + check([]string{"bls_keystore_path"}, "keystores/operator1.bls.keystore.json") + check([]string{"bls_keystore_password"}, "bpass1") + + // entry 2: operatorSet 1, suffix .2 + ks1 := ks.Content[1] + check2 := func(path []string, want string) { + n := migration.ResolveNode(ks1, path) + if n == nil || n.Value != want { + t.Errorf("expected %v == %q, got %v", path, want, n) + } + } + check2([]string{"avs"}, "0xAVS_ADDR") + check2([]string{"operatorSet"}, "1") + check2([]string{"ecdsa_keystore_path"}, "keystores/operator1.ecdsa.keystore.json") + check2([]string{"ecdsa_keystore_password"}, "pass1") + check2([]string{"bls_keystore_path"}, "keystores/operator1.bls.keystore.json") + check2([]string{"bls_keystore_password"}, "bpass1") + }) + + t.Run("operator 2 keystores created", func(t *testing.T) { + op := migration.ResolveNode(migrated, []string{"context", "operators", "1"}) + if op == nil { + t.Fatal("operator[1] missing") + } + if got := migration.ResolveNode(op, []string{"address"}); got == nil || got.Value != "0xOP2" { + t.Errorf("address not preserved, got %v", got) + } + + ks := migration.ResolveNode(op, []string{"keystores"}) + if ks == nil || ks.Kind != 2 || len(ks.Content) != 2 { + t.Fatalf("expected keystores sequence with 2 entries, got %#v", ks) + } + + ks0 := migration.ResolveNode(ks, []string{"0"}) + ks1 := migration.ResolveNode(ks, []string{"1"}) + + if n := migration.ResolveNode(ks0, []string{"operatorSet"}); n == nil || n.Value != "0" { + t.Errorf("operatorSet[0] expected 0, got %v", n) + } + if n := migration.ResolveNode(ks1, []string{"operatorSet"}); n == nil || n.Value != "1" { + t.Errorf("operatorSet[1] expected 1, got %v", n) + } + + // verify suffixing on operator2 + if n := migration.ResolveNode(ks0, []string{"ecdsa_keystore_path"}); n == nil || n.Value != "keystores/operator2.ecdsa.keystore.json" { + t.Errorf("unexpected ecdsa path[0]: %v", n) + } + if n := migration.ResolveNode(ks1, []string{"ecdsa_keystore_path"}); n == nil || n.Value != "keystores/operator2.ecdsa.keystore.json" { + t.Errorf("unexpected ecdsa path[1]: %v", n) + } + if n := migration.ResolveNode(ks0, []string{"bls_keystore_path"}); n == nil || n.Value != "keystores/operator2.bls.keystore.json" { + t.Errorf("unexpected bls path[0]: %v", n) + } + if n := migration.ResolveNode(ks1, []string{"bls_keystore_path"}); n == nil || n.Value != "keystores/operator2.bls.keystore.json" { + t.Errorf("unexpected bls path[1]: %v", n) + } + }) + + t.Run("allocations and context name preserved", func(t *testing.T) { + name := migration.ResolveNode(migrated, []string{"context", "name"}) + if name == nil || name.Value != "custom-context" { + t.Errorf("context name not preserved, got %v", name) + } + allocs := migration.ResolveNode(migrated, []string{"context", "allocations"}) + if allocs == nil || allocs.Kind != 2 || len(allocs.Content) != 1 { + t.Errorf("allocations mutated, got %#v", allocs) + } + }) +} + // TestAVSContextMigration_FullChain tests migrating through the entire chain from 0.0.1 to 0.0.8 func TestAVSContextMigration_FullChain(t *testing.T) { // Use the embedded v0.0.1 content as our starting point