Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd: create dkg cli to support partial deposits #2887

Merged
merged 3 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 2 additions & 33 deletions cmd/createcluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"os"
"path"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -250,8 +249,8 @@ func runCreateCluster(ctx context.Context, w io.Writer, conf clusterConfig) erro
return err
}

// Write deposit-data file
if err = writeDepositData(depositDatas, network, conf.ClusterDir, numNodes); err != nil {
// Write deposit-data files
if err = deposit.WriteClusterDepositDataFiles(depositDatas, network, conf.ClusterDir, numNodes); err != nil {
return err
}

Expand Down Expand Up @@ -597,36 +596,6 @@ func createDepositDatas(withdrawalAddresses []string, network string, secrets []
return signDepositDatas(secrets, withdrawalAddresses, network, depositAmounts)
}

// writeDepositData writes deposit data to disk for the DVs for all peers in a cluster.
func writeDepositData(depositDatas [][]eth2p0.DepositData, network string, clusterDir string, numNodes int) error {
// The loop across partial amounts (amounts will be unique)
for i := range depositDatas {
// Serialize the deposit data into bytes
bytes, err := deposit.MarshalDepositData(depositDatas[i], network)
if err != nil {
return err
}

if len(depositDatas[i]) == 0 {
return errors.New("empty deposit data at index", z.Int("index", i))
}

eth := float64(depositDatas[i][0].Amount) / float64(deposit.OneEthInGwei)
ethStr := strconv.FormatFloat(eth, 'f', -1, 64)
filename := fmt.Sprintf("deposit-data-%seth.json", ethStr)

for n := 0; n < numNodes; n++ {
depositPath := path.Join(nodeDir(clusterDir, n), filename)
err = os.WriteFile(depositPath, bytes, 0o400) // read-only
if err != nil {
return errors.Wrap(err, "write deposit data")
}
}
}

return nil
}

// createValidatorRegistrations creates a slice of builder validator registrations using the provided parameters and returns it.
func createValidatorRegistrations(feeAddresses []string, secrets []tbls.PrivateKey, forkVersion []byte, useCurrentTimestamp bool) ([]core.VersionedSignedValidatorRegistration, error) {
if len(feeAddresses) != len(secrets) {
Expand Down
17 changes: 14 additions & 3 deletions cmd/createdkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/cluster"
"github.com/obolnetwork/charon/eth2util"
"github.com/obolnetwork/charon/eth2util/deposit"
"github.com/obolnetwork/charon/eth2util/enr"
)

Expand All @@ -29,6 +30,7 @@ type createDKGConfig struct {
WithdrawalAddrs []string
Network string
DKGAlgo string
DepositAmounts []int // Amounts specified in ETH (integers).
OperatorENRs []string
}

Expand Down Expand Up @@ -61,6 +63,7 @@ func bindCreateDKGFlags(cmd *cobra.Command, config *createDKGConfig) {
cmd.Flags().StringSliceVar(&config.WithdrawalAddrs, "withdrawal-addresses", nil, "Comma separated list of Ethereum addresses to receive the returned stake and accrued rewards for each validator. Either provide a single withdrawal address or withdrawal addresses for each validator.")
cmd.Flags().StringVar(&config.Network, "network", defaultNetwork, "Ethereum network to create validators for. Options: mainnet, goerli, gnosis, sepolia, holesky.")
cmd.Flags().StringVar(&config.DKGAlgo, "dkg-algorithm", "default", "DKG algorithm to use; default, frost")
cmd.Flags().IntSliceVar(&config.DepositAmounts, "deposit-amounts", nil, "List of partial deposit amounts (integers) in ETH. Values must sum up to exactly 32ETH.")
cmd.Flags().StringSliceVar(&config.OperatorENRs, operatorENRs, nil, "[REQUIRED] Comma-separated list of each operator's Charon ENR address.")

mustMarkFlagRequired(cmd, operatorENRs)
Expand All @@ -79,7 +82,7 @@ func runCreateDKG(ctx context.Context, conf createDKGConfig) (err error) {
conf.Network = eth2util.Goerli.Name
}

if err = validateDKGConfig(conf.Threshold, len(conf.OperatorENRs), conf.Network); err != nil {
if err = validateDKGConfig(conf.Threshold, len(conf.OperatorENRs), conf.Network, conf.DepositAmounts); err != nil {
return err
}

Expand Down Expand Up @@ -124,7 +127,7 @@ func runCreateDKG(ctx context.Context, conf createDKGConfig) (err error) {
def, err := cluster.NewDefinition(
conf.Name, conf.NumValidators, conf.Threshold,
conf.FeeRecipientAddrs, conf.WithdrawalAddrs,
forkVersion, cluster.Creator{}, operators, nil, crand.Reader,
forkVersion, cluster.Creator{}, operators, conf.DepositAmounts, crand.Reader,
func(d *cluster.Definition) {
d.DKGAlgorithm = conf.DKGAlgo
})
Expand Down Expand Up @@ -175,7 +178,7 @@ func validateWithdrawalAddrs(addrs []string, network string) error {
}

// validateDKGConfig returns an error if any of the provided config parameter is invalid.
func validateDKGConfig(threshold, numOperators int, network string) error {
func validateDKGConfig(threshold, numOperators int, network string, depositAmounts []int) error {
if threshold > numOperators {
return errors.New("threshold cannot be greater than length of operators",
z.Int("threshold", threshold), z.Int("operators", numOperators))
Expand All @@ -190,6 +193,14 @@ func validateDKGConfig(threshold, numOperators int, network string) error {
return errors.New("unsupported network", z.Str("network", network))
}

if len(depositAmounts) > 0 {
amounts := deposit.EthsToGweis(depositAmounts)

if err := deposit.VerifyDepositAmounts(amounts); err != nil {
return err
}
}

return nil
}

Expand Down
12 changes: 9 additions & 3 deletions cmd/createdkg_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func TestCreateDkgValid(t *testing.T) {
WithdrawalAddrs: []string{validEthAddr},
Network: defaultNetwork,
DKGAlgo: "default",
DepositAmounts: []int{8, 16, 4, 4},
OperatorENRs: []string{
"enr:-JG4QFI0llFYxSoTAHm24OrbgoVx77dL6Ehl1Ydys39JYoWcBhiHrRhtGXDTaygWNsEWFb1cL7a1Bk0klIdaNuXplKWGAYGv0Gt7gmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQL6bcis0tFXnbqG4KuywxT5BLhtmijPFApKCDJNl3mXFYN0Y3CCDhqDdWRwgg4u",
"enr:-JG4QPnqHa7FU3PBqGxpV5L0hjJrTUqv8Wl6_UTHt-rELeICWjvCfcVfwmax8xI_eJ0ntI3ly9fgxAsmABud6-yBQiuGAYGv0iYPgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMLLCMZ5Oqi_sdnBfdyhmysZMfFm78PgF7Y9jitTJPSroN0Y3CCPoODdWRwgj6E",
Expand Down Expand Up @@ -182,21 +183,26 @@ func TestValidateDKGConfig(t *testing.T) {
t.Run("invalid threshold", func(t *testing.T) {
threshold := 5
numOperators := 4
err := validateDKGConfig(threshold, numOperators, "")
err := validateDKGConfig(threshold, numOperators, "", nil)
require.ErrorContains(t, err, "threshold cannot be greater than length of operators")
})

t.Run("insufficient ENRs", func(t *testing.T) {
threshold := 1
numOperators := 2
err := validateDKGConfig(threshold, numOperators, "")
err := validateDKGConfig(threshold, numOperators, "", nil)
require.ErrorContains(t, err, "insufficient operator ENRs")
})

t.Run("invalid network", func(t *testing.T) {
threshold := 3
numOperators := 4
err := validateDKGConfig(threshold, numOperators, "cosmos")
err := validateDKGConfig(threshold, numOperators, "cosmos", nil)
require.ErrorContains(t, err, "unsupported network")
})

t.Run("wrong deposit amounts sum", func(t *testing.T) {
err := validateDKGConfig(3, 4, "goerli", []int{8, 16})
require.ErrorContains(t, err, "sum of partial deposit amounts must sum up to 32ETH")
})
}
28 changes: 3 additions & 25 deletions dkg/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ import (
"path/filepath"
"strings"

eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/cluster"
"github.com/obolnetwork/charon/eth2util/deposit"
"github.com/obolnetwork/charon/eth2util/keymanager"
"github.com/obolnetwork/charon/eth2util/keystore"
"github.com/obolnetwork/charon/tbls"
Expand Down Expand Up @@ -159,26 +156,6 @@ func writeLock(datadir string, lock cluster.Lock) error {
return nil
}

// writeDepositData writes deposit data file to disk.
func writeDepositData(depositDatas []eth2p0.DepositData, network string, dataDir string) error {
// Serialize the deposit data into bytes
bytes, err := deposit.MarshalDepositData(depositDatas, network)
if err != nil {
return err
}

// Write it to disk
depositPath := path.Join(dataDir, "deposit-data.json")

//nolint:gosec // File needs to be read-only for everybody
err = os.WriteFile(depositPath, bytes, 0o444)
if err != nil {
return errors.Wrap(err, "write deposit data")
}

return nil
}

func checkClearDataDir(dataDir string) error {
// if dataDir is a file, return error
info, err := os.Stat(dataDir)
Expand All @@ -199,15 +176,16 @@ func checkClearDataDir(dataDir string) error {
disallowedEntities := map[string]struct{}{
"validator_keys": {},
"cluster-lock.json": {},
"deposit-data.json": {},
}

necessaryEntities := map[string]bool{
"charon-enr-private-key": false,
}

for _, entity := range dirContent {
if _, ok := disallowedEntities[entity.Name()]; ok {
isDepositData := strings.HasPrefix(entity.Name(), "deposit-data")

if _, disallowed := disallowedEntities[entity.Name()]; disallowed || isDepositData {
return errors.New("data directory not clean, cannot continue", z.Str("disallowed_entity", entity.Name()), z.Str("data-dir", dataDir))
}

Expand Down
4 changes: 2 additions & 2 deletions dkg/disk_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,15 @@ func TestCheckClearDataDir(t *testing.T) {
},
},
{
"dataDir contains deposit-data.json file",
"dataDir contains deposit-data-32eth.json file",
func(rootDir string, dataDir string) {
require.NoError(t,
os.Mkdir(filepath.Join(rootDir, dataDir), 0o755),
)

require.NoError(t,
os.WriteFile(
filepath.Join(rootDir, dataDir, "deposit-data.json"),
filepath.Join(rootDir, dataDir, "deposit-data-32eth.json"),
[]byte{1, 2, 3},
0o755,
),
Expand Down
4 changes: 2 additions & 2 deletions dkg/dkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,10 @@ func Run(ctx context.Context, conf Config) (err error) {
}
log.Debug(ctx, "Saved lock file to disk")

if err := writeDepositData(depositDatas, network, conf.DataDir); err != nil {
if err := deposit.WriteDepositDataFile(depositDatas, network, conf.DataDir); err != nil {
return err
}
log.Debug(ctx, "Saved deposit data file to disk")
log.Debug(ctx, "Saved deposit data file to disk", z.Str("filepath", deposit.GetDepositFilePath(conf.DataDir, depositDatas[0].Amount)))

// Signature verification and disk key write was step 6, advance to step 7
if err := nextStepSync(ctx); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion docs/dkg.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ No user input is required, charon does the work and outputs the following files
./cluster-lock.json # New lockfile based on cluster-definition.json with validator group public keys and public shares included with the initial cluster config
./charon/enr_private_key # Created before the ceremony took place [Back this up]
./charon/validator_keys/ # Folder of key shares to be backed up and moved to validator client [Back this up]
./charon/deposit_data # JSON file of deposit data for the distributed validators
./charon/deposit_data* # JSON files of deposit data for the distributed validators
./charon/exit_data # JSON file of exit data that ethdo can broadcast
```

Expand Down
63 changes: 62 additions & 1 deletion eth2util/deposit/deposit.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path"
"sort"
"strconv"
"strings"

eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
Expand Down Expand Up @@ -219,7 +222,7 @@
sum += amount
}

if sum > MaxDepositAmount {
if sum != MaxDepositAmount {
return errors.New("sum of partial deposit amounts must sum up to 32ETH", z.U64("sum", uint64(sum)))
}

Expand All @@ -241,3 +244,61 @@

return gweiAmounts
}

// WriteClusterDepositDataFiles writes deposit-data-*eth.json files for each distinct amount.
func WriteClusterDepositDataFiles(depositDatas [][]eth2p0.DepositData, network string, clusterDir string, numNodes int) error {
// The loop across partial amounts (shall be unique)
for _, dd := range depositDatas {
for n := 0; n < numNodes; n++ {
nodeDir := path.Join(clusterDir, fmt.Sprintf("node%d", n))
if err := WriteDepositDataFile(dd, network, nodeDir); err != nil {
return err
}

Check warning on line 256 in eth2util/deposit/deposit.go

View check run for this annotation

Codecov / codecov/patch

eth2util/deposit/deposit.go#L255-L256

Added lines #L255 - L256 were not covered by tests
}
}

return nil
}

// WriteDepositDataFile writes deposit-data-*eth.json file for the provided depositDatas.
// The amount will be reflected in the filename in ETH.
// All depositDatas amounts shall have equal values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All depositDatas amounts shall have equal values.
I think it should say All depositDatas amounts should sum up to 32 ETH., right? It is not mandatory to have equal values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in this case. This function writes the single deposit file for a node. In case of partial deposits this function is called multiple times per each partial amount x each node. The input slice here is the input for the single file. All amounts shall be equal.

func WriteDepositDataFile(depositDatas []eth2p0.DepositData, network string, dataDir string) error {
if len(depositDatas) == 0 {
return errors.New("empty deposit data")
}

for i, dd := range depositDatas {
if i == 0 {
continue
}

if depositDatas[0].Amount != dd.Amount {
return errors.New("deposit datas has different amount", z.Int("index", i))
}
}

bytes, err := MarshalDepositData(depositDatas, network)
if err != nil {
return err
}

Check warning on line 284 in eth2util/deposit/deposit.go

View check run for this annotation

Codecov / codecov/patch

eth2util/deposit/deposit.go#L283-L284

Added lines #L283 - L284 were not covered by tests

depositFilePath := GetDepositFilePath(dataDir, depositDatas[0].Amount)

//nolint:gosec // File needs to be read-only for everybody
err = os.WriteFile(depositFilePath, bytes, 0o444)
if err != nil {
return errors.Wrap(err, "write deposit data")
}

Check warning on line 292 in eth2util/deposit/deposit.go

View check run for this annotation

Codecov / codecov/patch

eth2util/deposit/deposit.go#L291-L292

Added lines #L291 - L292 were not covered by tests

return nil
}

// GetDepositFilePath constructs and return deposit-data file path.
func GetDepositFilePath(dataDir string, amount eth2p0.Gwei) string {
eth := float64(amount) / float64(OneEthInGwei)
ethStr := strconv.FormatFloat(eth, 'f', -1, 64)
filename := fmt.Sprintf("deposit-data-%seth.json", ethStr)

return path.Join(dataDir, filename)
}
Loading
Loading