From f929f59dccad23115e56b3337db0118d2ad54fe9 Mon Sep 17 00:00:00 2001 From: Andrei Smirnov Date: Mon, 29 Jan 2024 14:08:53 +0300 Subject: [PATCH] cmd: --deposit-amounts flag (#2820) Added `--deposit-amounts` flag to cmd interface for `create cluster` and `dkg` commands in according with the spec. category: feature ticket: none --- cmd/createcluster.go | 15 ++++- cmd/createcluster_internal_test.go | 20 +++++++ ...Cluster_four_partial_deposits_files.golden | 22 +++++++ ...luster_four_partial_deposits_output.golden | 11 ++++ ...eCluster_two_partial_deposits_files.golden | 22 +++++++ ...Cluster_two_partial_deposits_output.golden | 11 ++++ eth2util/deposit/deposit.go | 45 ++++++++++++++ eth2util/deposit/deposit_test.go | 58 +++++++++++++++++++ 8 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 cmd/testdata/TestCreateCluster_four_partial_deposits_files.golden create mode 100644 cmd/testdata/TestCreateCluster_four_partial_deposits_output.golden create mode 100644 cmd/testdata/TestCreateCluster_two_partial_deposits_files.golden create mode 100644 cmd/testdata/TestCreateCluster_two_partial_deposits_output.golden diff --git a/cmd/createcluster.go b/cmd/createcluster.go index e0bd57033..604de9368 100644 --- a/cmd/createcluster.go +++ b/cmd/createcluster.go @@ -69,6 +69,8 @@ type clusterConfig struct { Network string NumDVs int + DepositAmounts []int // Amounts specified in ETH (integers). + SplitKeys bool SplitKeysDir string @@ -86,7 +88,7 @@ func newCreateClusterCmd(runFunc func(context.Context, io.Writer, clusterConfig) cmd := &cobra.Command{ Use: "cluster", Short: "Create private keys and configuration files needed to run a distributed validator cluster locally", - Long: "Creates a local charon cluster configuration including validator keys, charon p2p keys, cluster-lock.json and a deposit-data.json. " + + Long: "Creates a local charon cluster configuration including validator keys, charon p2p keys, cluster-lock.json and deposit-data.json file(s). " + "See flags for supported features.", RunE: func(cmd *cobra.Command, args []string) error { return runFunc(cmd.Context(), cmd.OutOrStdout(), conf) @@ -119,6 +121,7 @@ func bindClusterFlags(flags *pflag.FlagSet, config *clusterConfig) { flags.StringVar(&config.testnetConfig.GenesisForkVersionHex, "testnet-fork-version", "", "Genesis fork version of the custom test network (in hex).") flags.Uint64Var(&config.testnetConfig.ChainID, "testnet-chain-id", 0, "Chain ID of the custom test network.") flags.Int64Var(&config.testnetConfig.GenesisTimestamp, "testnet-genesis-timestamp", 0, "Genesis timestamp of the custom test network.") + flags.IntSliceVar(&config.DepositAmounts, "deposit-amounts", nil, "List of partial deposit amounts (integers) in ETH. Values must sum up to exactly 32ETH.") } func bindInsecureFlags(flags *pflag.FlagSet, insecureKeys *bool) { @@ -321,6 +324,16 @@ func validateCreateConfig(ctx context.Context, conf clusterConfig) error { return errors.New("number of --keymanager-addresses do not match --keymanager-auth-tokens. Please fix configuration flags") } + if len(conf.DepositAmounts) > 0 { + amounts := deposit.EthsToGweis(conf.DepositAmounts) + + if err := deposit.VerifyDepositAmounts(amounts); err != nil { + return err + } + + log.Warn(ctx, "Partial deposits feature is under development. The --deposit-amounts flag has no effect yet.", nil) + } + for _, addr := range conf.KeymanagerAddrs { keymanagerURL, err := url.ParseRequestURI(addr) if err != nil { diff --git a/cmd/createcluster_internal_test.go b/cmd/createcluster_internal_test.go index ba41ace53..8356f501e 100644 --- a/cmd/createcluster_internal_test.go +++ b/cmd/createcluster_internal_test.go @@ -52,6 +52,26 @@ func TestCreateCluster(t *testing.T) { Network: eth2util.Goerli.Name, }, }, + { + Name: "two partial deposits", + Config: clusterConfig{ + NumNodes: 4, + Threshold: 3, + NumDVs: 1, + Network: eth2util.Goerli.Name, + DepositAmounts: []int{31, 1}, + }, + }, + { + Name: "four partial deposits", + Config: clusterConfig{ + NumNodes: 4, + Threshold: 3, + NumDVs: 1, + Network: eth2util.Goerli.Name, + DepositAmounts: []int{8, 8, 8, 8}, + }, + }, { Name: "splitkeys", Config: clusterConfig{ diff --git a/cmd/testdata/TestCreateCluster_four_partial_deposits_files.golden b/cmd/testdata/TestCreateCluster_four_partial_deposits_files.golden new file mode 100644 index 000000000..153b92536 --- /dev/null +++ b/cmd/testdata/TestCreateCluster_four_partial_deposits_files.golden @@ -0,0 +1,22 @@ +[ + "node0", + "node1", + "node2", + "node3", + "node0/charon-enr-private-key", + "node0/cluster-lock.json", + "node0/deposit-data.json", + "node0/validator_keys", + "node1/charon-enr-private-key", + "node1/cluster-lock.json", + "node1/deposit-data.json", + "node1/validator_keys", + "node2/charon-enr-private-key", + "node2/cluster-lock.json", + "node2/deposit-data.json", + "node2/validator_keys", + "node3/charon-enr-private-key", + "node3/cluster-lock.json", + "node3/deposit-data.json", + "node3/validator_keys" +] \ No newline at end of file diff --git a/cmd/testdata/TestCreateCluster_four_partial_deposits_output.golden b/cmd/testdata/TestCreateCluster_four_partial_deposits_output.golden new file mode 100644 index 000000000..163ce162a --- /dev/null +++ b/cmd/testdata/TestCreateCluster_four_partial_deposits_output.golden @@ -0,0 +1,11 @@ +Created charon cluster: + --split-existing-keys=false + +charon/ +├─ node[0-3]/ Directory for each node +│ ├─ charon-enr-private-key Charon networking private key for node authentication +│ ├─ cluster-lock.json Cluster lock defines the cluster lock file which is signed by all nodes +│ ├─ deposit-data.json Deposit data file is used to activate a Distributed Validator on DV Launchpad +│ ├─ validator_keys Validator keystores and password +│ │ ├─ keystore-*.json Validator private share key for duty signing +│ │ ├─ keystore-*.txt Keystore password files for keystore-*.json diff --git a/cmd/testdata/TestCreateCluster_two_partial_deposits_files.golden b/cmd/testdata/TestCreateCluster_two_partial_deposits_files.golden new file mode 100644 index 000000000..153b92536 --- /dev/null +++ b/cmd/testdata/TestCreateCluster_two_partial_deposits_files.golden @@ -0,0 +1,22 @@ +[ + "node0", + "node1", + "node2", + "node3", + "node0/charon-enr-private-key", + "node0/cluster-lock.json", + "node0/deposit-data.json", + "node0/validator_keys", + "node1/charon-enr-private-key", + "node1/cluster-lock.json", + "node1/deposit-data.json", + "node1/validator_keys", + "node2/charon-enr-private-key", + "node2/cluster-lock.json", + "node2/deposit-data.json", + "node2/validator_keys", + "node3/charon-enr-private-key", + "node3/cluster-lock.json", + "node3/deposit-data.json", + "node3/validator_keys" +] \ No newline at end of file diff --git a/cmd/testdata/TestCreateCluster_two_partial_deposits_output.golden b/cmd/testdata/TestCreateCluster_two_partial_deposits_output.golden new file mode 100644 index 000000000..163ce162a --- /dev/null +++ b/cmd/testdata/TestCreateCluster_two_partial_deposits_output.golden @@ -0,0 +1,11 @@ +Created charon cluster: + --split-existing-keys=false + +charon/ +├─ node[0-3]/ Directory for each node +│ ├─ charon-enr-private-key Charon networking private key for node authentication +│ ├─ cluster-lock.json Cluster lock defines the cluster lock file which is signed by all nodes +│ ├─ deposit-data.json Deposit data file is used to activate a Distributed Validator on DV Launchpad +│ ├─ validator_keys Validator keystores and password +│ │ ├─ keystore-*.json Validator private share key for duty signing +│ │ ├─ keystore-*.txt Keystore password files for keystore-*.json diff --git a/eth2util/deposit/deposit.go b/eth2util/deposit/deposit.go index 3706f33e8..f0875c5f6 100644 --- a/eth2util/deposit/deposit.go +++ b/eth2util/deposit/deposit.go @@ -18,6 +18,12 @@ import ( "github.com/obolnetwork/charon/tbls" ) +const ( + minSingleDepositAmountGwei = 1000000000 // 1ETH + maxTotalDepositAmountGwei = 32000000000 // 32ETH + oneEthInGwei = 1000000000 // 1ETH in Gwei +) + var ( // Minimum allowed deposit amount (1ETH). MinValidatorAmount = eth2p0.Gwei(1000000000) @@ -197,3 +203,42 @@ type depositDataJSON struct { NetworkName string `json:"network_name"` DepositCliVersion string `json:"deposit_cli_version"` } + +// VerifyDepositAmounts verifies various conditions about partial deposits rules. +func VerifyDepositAmounts(amounts []eth2p0.Gwei) error { + if len(amounts) == 0 { + // If no partial amounts specified, the implementation shall default to 32ETH. + return nil + } + + var sum eth2p0.Gwei + for _, amount := range amounts { + if amount < minSingleDepositAmountGwei { + return errors.New("each partial deposit amount must be greater than 1ETH", z.U64("amount", uint64(amount))) + } + + sum += amount + } + + if sum > eth2p0.Gwei(maxTotalDepositAmountGwei) { + return errors.New("sum of partial deposit amounts must sum up to 32ETH", z.U64("sum", uint64(sum))) + } + + return nil +} + +// EthsToGweis converts amounts from []int (ETH) to []eth2p0.Gwei. +// For verification, please see VerifyDepositAmounts(). +func EthsToGweis(ethAmounts []int) []eth2p0.Gwei { + if ethAmounts == nil { + return nil + } + + var gweiAmounts []eth2p0.Gwei + for _, ethAmount := range ethAmounts { + gwei := eth2p0.Gwei(oneEthInGwei * ethAmount) + gweiAmounts = append(gweiAmounts, gwei) + } + + return gweiAmounts +} diff --git a/eth2util/deposit/deposit_test.go b/eth2util/deposit/deposit_test.go index 1309c7df6..b2555f1b0 100644 --- a/eth2util/deposit/deposit_test.go +++ b/eth2util/deposit/deposit_test.go @@ -105,3 +105,61 @@ func GetKeys(t *testing.T, privKey string) (tbls.PrivateKey, eth2p0.BLSPubKey) { return sk, pubkey } + +func TestVerifyDepositAmounts(t *testing.T) { + t.Run("empty slice", func(t *testing.T) { + err := deposit.VerifyDepositAmounts(nil) + + require.NoError(t, err) + }) + + t.Run("valid amounts", func(t *testing.T) { + amounts := []eth2p0.Gwei{ + eth2p0.Gwei(16000000000), + eth2p0.Gwei(16000000000), + } + + err := deposit.VerifyDepositAmounts(amounts) + + require.NoError(t, err) + }) + + t.Run("each amount is greater than 1ETH", func(t *testing.T) { + amounts := []eth2p0.Gwei{ + eth2p0.Gwei(500000000), // 0.5ETH + eth2p0.Gwei(31500000000), // 31.5ETH + } + + err := deposit.VerifyDepositAmounts(amounts) + + require.ErrorContains(t, err, "each partial deposit amount must be greater than 1ETH") + }) + + t.Run("total sum is 32ETH", func(t *testing.T) { + amounts := []eth2p0.Gwei{ + eth2p0.Gwei(1000000000), + eth2p0.Gwei(32000000000), + } + + err := deposit.VerifyDepositAmounts(amounts) + + require.ErrorContains(t, err, "sum of partial deposit amounts must sum up to 32ETH") + }) +} + +func TestEthsToGweis(t *testing.T) { + t.Run("nil slice", func(t *testing.T) { + slice := deposit.EthsToGweis(nil) + + require.Nil(t, slice) + }) + + t.Run("values", func(t *testing.T) { + slice := deposit.EthsToGweis([]int{1, 5}) + + require.Equal(t, []eth2p0.Gwei{ + eth2p0.Gwei(1000000000), + eth2p0.Gwei(5000000000), + }, slice) + }) +}