diff --git a/cli/commands/deposit/create.go b/cli/commands/deposit/create.go index d25528ade..2d98ef30b 100644 --- a/cli/commands/deposit/create.go +++ b/cli/commands/deposit/create.go @@ -39,26 +39,33 @@ import ( ) const ( - createAddr0 = iota - createAmt1 = iota - createRoot2 = iota - createArgsCount = iota + createAddr0 = iota + createAmt1 = iota + createRoot2 = iota - overrideNodeKey = "override-node-key" - valPrivateKey = "validator-private-key" + minArgsCreateDeposit = 2 + maxArgsCreateDeposit = 3 + + overrideNodeKey = "override-node-key" + valPrivateKey = "validator-private-key" + useGenesisValidatorRoot = "genesis-validator-root" + + useGenesisValidatorRootShorthand = "g" + + defaultGenesisValidatorRoot = "" ) // NewCreateValidator creates a new command to create a validator deposit. // -//nolint:lll // reads better if long description is one line +//nolint:lll // reads better if long description is one line. func NewCreateValidator( chainSpec chain.Spec, ) *cobra.Command { cmd := &cobra.Command{ - Use: "create-validator", + Use: "create-validator [withdrawal-address] [amount] ?[beacond/genesis.json]", Short: "Creates a validator deposit", - Long: `Creates a validator deposit with the necessary credentials. The arguments are expected in the order of withdrawal address, deposit amount, and genesis validator root. If the broadcast flag is set to true, a private key must be provided to sign the transaction.`, - Args: cobra.ExactArgs(createArgsCount), + Long: `Creates a validator deposit with the necessary credentials. The arguments are expected in the order of withdrawal address, deposit amount, and optionally the beacond genesis file. If the genesis validator root flag is NOT set, the beacond genesis file MUST be provided as the last argument. If the broadcast flag is set to true, a private key must be provided to sign the transaction.`, + Args: cobra.RangeArgs(minArgsCreateDeposit, maxArgsCreateDeposit), RunE: createValidatorCmd(chainSpec), } @@ -73,6 +80,12 @@ func NewCreateValidator( "", // no default private key "validator private key. This is required if the override-node-key flag is set.", ) + cmd.Flags().StringP( + useGenesisValidatorRoot, + useGenesisValidatorRootShorthand, + defaultGenesisValidatorRoot, + "Use the provided genesis validator root. If this is not set, the beacond genesis file must be provided manually as the last argument.", + ) return cmd } @@ -101,8 +114,9 @@ func createValidatorCmd( return err } - genValRootStr := args[createRoot2] - genesisValidatorRoot, err := parser.ConvertGenesisValidatorRoot(genValRootStr) + genesisValidatorRoot, err := getGenesisValidatorRoot( + cmd, chainSpec, args, maxArgsCreateDeposit, + ) if err != nil { return err } diff --git a/cli/commands/deposit/utils.go b/cli/commands/deposit/utils.go new file mode 100644 index 000000000..1e290b7c6 --- /dev/null +++ b/cli/commands/deposit/utils.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: BUSL-1.1 +// +// Copyright (C) 2025, Berachain Foundation. All rights reserved. +// Use of this software is governed by the Business Source License included +// in the LICENSE file of this repository and at www.mariadb.com/bsl11. +// +// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY +// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER +// VERSIONS OF THE LICENSED WORK. +// +// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF +// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF +// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE). +// +// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +// AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +// TITLE. + +package deposit + +import ( + "github.com/berachain/beacon-kit/chain" + "github.com/berachain/beacon-kit/cli/utils/genesis" + "github.com/berachain/beacon-kit/cli/utils/parser" + "github.com/berachain/beacon-kit/errors" + "github.com/berachain/beacon-kit/primitives/common" + "github.com/spf13/cobra" +) + +// Get the genesis validator root. If the genesis validator root flag is not set, the genesis +// validator root is computed from the genesis file at the last argument (idx: maxArgs - 1). +func getGenesisValidatorRoot( + cmd *cobra.Command, chainSpec chain.Spec, args []string, maxArgs int, +) (common.Root, error) { + var genesisValidatorRoot common.Root + genesisValidatorRootStr, err := cmd.Flags().GetString(useGenesisValidatorRoot) + if err != nil { + return common.Root{}, err + } + + if genesisValidatorRootStr != defaultGenesisValidatorRoot { + genesisValidatorRoot, err = parser.ConvertGenesisValidatorRoot(genesisValidatorRootStr) + } else { + if len(args) != maxArgs { + return common.Root{}, errors.New( + "genesis validator root is required if not using the genesis file flag", + ) + } + genesisValidatorRoot, err = genesis.ComputeValidatorsRootFromFile(args[maxArgs-1], chainSpec) + } + + return genesisValidatorRoot, err +} diff --git a/cli/commands/deposit/validate.go b/cli/commands/deposit/validate.go index e27e5aa7f..d6c7099f5 100644 --- a/cli/commands/deposit/validate.go +++ b/cli/commands/deposit/validate.go @@ -34,12 +34,13 @@ import ( ) const ( - validatePubKey0 = iota - validateCreds1 = iota - validateAmt2 = iota - validateSign3 = iota - validateRoot4 = iota - validateArgsCount = iota + validatePubKey0 = iota + validateCreds1 = iota + validateAmt2 = iota + validateSign3 = iota + + minArgsValidateDeposit = 4 + maxArgsValidateDeposit = 5 ) // NewValidateDeposit creates a new command for validating a deposit message. @@ -47,23 +48,29 @@ const ( //nolint:lll // reads better if long description is one line func NewValidateDeposit(chainSpec chain.Spec) *cobra.Command { cmd := &cobra.Command{ - Use: "validate", + Use: "validate [pubkey] [withdrawal-credentials] [amount] [signature] ?[beacond/genesis.json]", Short: "Validates a deposit message for creating a new validator", - Long: `Validates a deposit message for creating a new validator. The deposit message includes the public key, withdrawal credentials, and deposit amount. The args taken are in the order of the public key, withdrawal credentials, deposit amount, signature, and genesis validator root.`, - Args: cobra.ExactArgs(validateArgsCount), + Long: `Validates a deposit message (public key, withdrawal credentials, deposit amount) for creating a new validator. The args taken are in the order of the public key, withdrawal credentials, deposit amount, signature, and optionally the beacond genesis file. If the genesis validator root flag is NOT set, the beacond genesis file MUST be provided as the last argument.`, + Args: cobra.RangeArgs(minArgsValidateDeposit, maxArgsValidateDeposit), RunE: validateDepositMessage(chainSpec), } + cmd.Flags().StringP( + useGenesisValidatorRoot, + useGenesisValidatorRootShorthand, + defaultGenesisValidatorRoot, + "Use the provided genesis validator root. If this is not set, the beacond genesis file must be provided manually as the last argument.", + ) + return cmd } -// validateDepositMessage validates a deposit message for creating a new -// validator. +// validateDepositMessage validates a deposit message for creating a new validator. func validateDepositMessage(chainSpec chain.Spec) func( _ *cobra.Command, args []string, ) error { - return func(_ *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { pubKeyStr := args[validatePubKey0] pubkey, err := parser.ConvertPubkey(pubKeyStr) if err != nil { @@ -88,8 +95,9 @@ func validateDepositMessage(chainSpec chain.Spec) func( return err } - genValRootStr := args[validateRoot4] - genesisValidatorRoot, err := parser.ConvertGenesisValidatorRoot(genValRootStr) + genesisValidatorRoot, err := getGenesisValidatorRoot( + cmd, chainSpec, args, maxArgsValidateDeposit, + ) if err != nil { return err } diff --git a/cli/commands/genesis/deposit.go b/cli/commands/genesis/deposit.go index f0661e60e..9de3578f5 100644 --- a/cli/commands/genesis/deposit.go +++ b/cli/commands/genesis/deposit.go @@ -47,7 +47,7 @@ import ( // AddGenesisDepositCmd - returns the cobra command to // add a premined deposit to the genesis file. // -//nolint:lll // reads better if long description is one line +//nolint:lll // reads better if long description is one line. func AddGenesisDepositCmd(cs chain.Spec) *cobra.Command { cmd := &cobra.Command{ Use: "add-premined-deposit", diff --git a/cli/commands/genesis/root.go b/cli/commands/genesis/root.go index 2e828b61f..1ed898539 100644 --- a/cli/commands/genesis/root.go +++ b/cli/commands/genesis/root.go @@ -22,80 +22,27 @@ package genesis import ( "github.com/berachain/beacon-kit/chain" - "github.com/berachain/beacon-kit/consensus-types/types" - "github.com/berachain/beacon-kit/errors" - "github.com/berachain/beacon-kit/primitives/common" - "github.com/berachain/beacon-kit/primitives/encoding/json" - "github.com/berachain/beacon-kit/primitives/math" - "github.com/spf13/afero" + "github.com/berachain/beacon-kit/cli/utils/genesis" "github.com/spf13/cobra" ) -// Beacon, AppState and Genesis are code duplications that -// collectively reproduce part of genesis file structure - -type Beacon struct { - Deposits types.Deposits `json:"deposits"` -} - -type AppState struct { - Beacon `json:"beacon"` -} - -type Genesis struct { - AppState `json:"app_state"` -} - -// TODO: move this logic to the `deposit create-validator/validate` commands as it is only -// required there. +// GetGenesisValidatorRootCmd returns a command that gets the genesis validator root from a given +// beacond genesis file. func GetGenesisValidatorRootCmd(cs chain.Spec) *cobra.Command { cmd := &cobra.Command{ Use: "validator-root [beacond/genesis.json]", Short: "gets and returns the genesis validator root", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - // Read the genesis file. - genesisBz, err := afero.ReadFile(afero.NewOsFs(), args[0]) - if err != nil { - return errors.Wrap(err, "failed to genesis json file") - } - - var genesis Genesis - // Unmarshal JSON data into the Genesis struct - err = json.Unmarshal(genesisBz, &genesis) + genesisValidatorsRoot, err := genesis.ComputeValidatorsRootFromFile(args[0], cs) if err != nil { - return errors.Wrap(err, "failed to unmarshal JSON") + return err } - validatorHashTreeRoot := ValidatorsRoot(genesis.Deposits, cs) - cmd.Printf("%s\n", validatorHashTreeRoot) + cmd.Printf("%s\n", genesisValidatorsRoot) return nil }, } return cmd } - -func ValidatorsRoot(deposits types.Deposits, cs chain.Spec) common.Root { - validators := make(types.Validators, len(deposits)) - minEffectiveBalance := math.Gwei(cs.EjectionBalance() + cs.EffectiveBalanceIncrement()) - - for i, deposit := range deposits { - val := types.NewValidatorFromDeposit( - deposit.Pubkey, - deposit.Credentials, - deposit.Amount, - math.Gwei(cs.EffectiveBalanceIncrement()), - math.Gwei(cs.MaxEffectiveBalance()), - ) - - // mimic processGenesisActivation - if val.GetEffectiveBalance() >= minEffectiveBalance { - val.SetActivationEligibilityEpoch(0) - val.SetActivationEpoch(0) - } - validators[i] = val - } - - return validators.HashTreeRoot() -} diff --git a/cli/utils/genesis/root.go b/cli/utils/genesis/root.go new file mode 100644 index 000000000..ecb18b3cd --- /dev/null +++ b/cli/utils/genesis/root.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: BUSL-1.1 +// +// Copyright (C) 2025, Berachain Foundation. All rights reserved. +// Use of this software is governed by the Business Source License included +// in the LICENSE file of this repository and at www.mariadb.com/bsl11. +// +// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY +// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER +// VERSIONS OF THE LICENSED WORK. +// +// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF +// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF +// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE). +// +// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +// AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +// TITLE. + +package genesis + +import ( + "github.com/berachain/beacon-kit/chain" + "github.com/berachain/beacon-kit/consensus-types/types" + "github.com/berachain/beacon-kit/errors" + "github.com/berachain/beacon-kit/primitives/common" + "github.com/berachain/beacon-kit/primitives/encoding/json" + "github.com/berachain/beacon-kit/primitives/math" + "github.com/spf13/afero" +) + +// Beacon, AppState and Genesis are code duplications that +// collectively reproduce part of genesis file structure + +type Beacon struct { + Deposits types.Deposits `json:"deposits"` +} + +type AppState struct { + Beacon `json:"beacon"` +} + +type Genesis struct { + AppState `json:"app_state"` +} + +// ComputeValidatorsRootFromFile returns the validator root for a given genesis file and chain spec. +func ComputeValidatorsRootFromFile(genesisFile string, cs chain.Spec) (common.Root, error) { + genesisBz, err := afero.ReadFile(afero.NewOsFs(), genesisFile) + if err != nil { + return common.Root{}, errors.Wrap(err, "failed to genesis json file") + } + + var appGenesis Genesis + err = json.Unmarshal(genesisBz, &appGenesis) + if err != nil { + return common.Root{}, errors.Wrap(err, "failed to unmarshal JSON") + } + + return ComputeValidatorsRoot(appGenesis.Deposits, cs), nil +} + +// ComputeValidatorsRoot returns the validator root for a given set of genesis deposits +// and a chain spec. +func ComputeValidatorsRoot(genesisDeposits types.Deposits, cs chain.Spec) common.Root { + validators := make(types.Validators, len(genesisDeposits)) + minEffectiveBalance := math.Gwei(cs.EjectionBalance() + cs.EffectiveBalanceIncrement()) + + for i, deposit := range genesisDeposits { + val := types.NewValidatorFromDeposit( + deposit.Pubkey, + deposit.Credentials, + deposit.Amount, + math.Gwei(cs.EffectiveBalanceIncrement()), + math.Gwei(cs.MaxEffectiveBalance()), + ) + + // mimic processGenesisActivation + if val.GetEffectiveBalance() >= minEffectiveBalance { + val.SetActivationEligibilityEpoch(0) + val.SetActivationEpoch(0) + } + validators[i] = val + } + + return validators.HashTreeRoot() +} diff --git a/cli/commands/genesis/root_test.go b/cli/utils/genesis/root_test.go similarity index 96% rename from cli/commands/genesis/root_test.go rename to cli/utils/genesis/root_test.go index d77850ed1..16cc9327c 100644 --- a/cli/commands/genesis/root_test.go +++ b/cli/utils/genesis/root_test.go @@ -31,7 +31,7 @@ import ( "testing" "testing/quick" - "github.com/berachain/beacon-kit/cli/commands/genesis" + "github.com/berachain/beacon-kit/cli/utils/genesis" "github.com/berachain/beacon-kit/config/spec" "github.com/berachain/beacon-kit/consensus-types/types" "github.com/berachain/beacon-kit/primitives/common" @@ -96,7 +96,7 @@ func TestCompareGenesisCmdWithStateProcessor(t *testing.T) { } } // genesis validators root from CLI - cliValRoot := genesis.ValidatorsRoot(deposits, cs) + cliValRoot := genesis.ComputeValidatorsRoot(deposits, cs) // genesis validators root from StateProcessor sp, st, _, _ := statetransition.SetupTestState(t, cs)