diff --git a/cmd/root.go b/cmd/root.go index c1bd606..502cde3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,6 +18,8 @@ import ( "errors" "fmt" "os" + "regexp" + "sort" "strings" "time" @@ -264,6 +266,47 @@ func accountFromPath(path string) (wtypes.Account, error) { return wallet.AccountByName(accountName) } +// accountsFromPath obtains 0 or more accounts given a path specification. +func accountsFromPath(path string) ([]wtypes.Account, error) { + accounts := make([]wtypes.Account, 0) + + // Quick check to see if it's a single account + account, err := accountFromPath(path) + if err == nil && account != nil { + accounts = append(accounts, account) + return accounts, nil + } + + wallet, err := walletFromPath(path) + if err != nil { + return nil, err + } + _, accountSpec, err := walletAndAccountNamesFromPath(path) + if err != nil { + return nil, err + } + + if accountSpec == "" { + accountSpec = "^.*$" + } else { + accountSpec = fmt.Sprintf("^%s$", accountSpec) + } + re := regexp.MustCompile(accountSpec) + + for account := range wallet.Accounts() { + if re.Match([]byte(account.Name())) { + accounts = append(accounts, account) + } + } + + // Tidy up accounts by name. + sort.Slice(accounts, func(i, j int) bool { + return accounts[i].Name() < accounts[j].Name() + }) + + return accounts, nil +} + // sign signs data in a domain. func sign(account wtypes.Account, data []byte, domain uint64) (types.Signature, error) { if !account.IsUnlocked() { diff --git a/cmd/validatordepositdata.go b/cmd/validatordepositdata.go index 57a1c07..55fcfe8 100644 --- a/cmd/validatordepositdata.go +++ b/cmd/validatordepositdata.go @@ -16,6 +16,7 @@ package cmd import ( "fmt" "os" + "strings" "github.com/prysmaticlabs/go-ssz" "github.com/spf13/cobra" @@ -27,22 +28,35 @@ import ( var validatorDepositDataValidatorAccount string var validatorDepositDataWithdrawalAccount string var validatorDepositDataDepositValue string +var validatorDepositDataRaw bool var validatorDepositDataCmd = &cobra.Command{ Use: "depositdata", - Short: "Generate deposit data for a validator", - Long: `Generate data for a deposit to the Ethereum 1 validator contract. For example: + Short: "Generate deposit data for one or more validators", + Long: `Generate data for deposits to the Ethereum 1 validator contract. For example: ethdo validator depositdata --validatoraccount=primary/validator --withdrawalaccount=primary/current --value="32 Ether" +If validatoraccount is provided with an account path it will generate deposit data for all matching accounts. + The information generated can be passed to ethereal to create a deposit from the Ethereum 1 chain. In quiet mode this will return 0 if the the data can be generated correctly, otherwise 1.`, Run: func(cmd *cobra.Command, args []string) { assert(validatorDepositDataValidatorAccount != "", "--validatoraccount is required") - validatorAccount, err := accountFromPath(validatorDepositDataValidatorAccount) + validatorWallet, err := walletFromPath(validatorDepositDataValidatorAccount) + errCheck(err, "Failed to obtain validator wallet") + validatorAccounts, err := accountsFromPath(validatorDepositDataValidatorAccount) errCheck(err, "Failed to obtain validator account") - outputIf(debug, fmt.Sprintf("Validator public key is %048x", validatorAccount.PublicKey().Marshal())) + assert(len(validatorAccounts) > 0, "Failed to obtain validator account") + if len(validatorAccounts) == 1 { + outputIf(debug, fmt.Sprintf("Validator public key is %048x", validatorAccounts[0].PublicKey().Marshal())) + } else { + for _, validatorAccount := range validatorAccounts { + outputIf(verbose, fmt.Sprintf("Creating deposit for %s/%s", validatorWallet.Name(), validatorAccount.Name())) + outputIf(debug, fmt.Sprintf("Validator public key is %048x", validatorAccount.PublicKey().Marshal())) + } + } assert(validatorDepositDataWithdrawalAccount != "", "--withdrawalaccount is required") withdrawalAccount, err := accountFromPath(validatorDepositDataWithdrawalAccount) @@ -59,42 +73,84 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth errCheck(err, "Invalid value") assert(val >= 1000000000, "deposit value must be at least 1 Ether") - depositData := struct { - PubKey []byte `ssz-size:"48"` - WithdrawalCredentials []byte `ssz-size:"32"` - Value uint64 - }{ - PubKey: validatorAccount.PublicKey().Marshal(), - WithdrawalCredentials: withdrawalCredentials, - Value: val, - } - signingRoot, err := ssz.HashTreeRoot(depositData) - errCheck(err, "Failed to generate deposit data signing root") - outputIf(debug, fmt.Sprintf("Signing root is %x", signingRoot)) - domain := types.Domain(types.DomainDeposit, []byte{0, 0, 0, 0}) - signature, err := sign(validatorAccount, signingRoot[:], domain) - errCheck(err, "Failed to sign deposit data signing root") + // For each key, generate deposit data + outputs := make([]string, 0) + for _, validatorAccount := range validatorAccounts { + depositData := struct { + PubKey []byte `ssz-size:"48"` + WithdrawalCredentials []byte `ssz-size:"32"` + Value uint64 + }{ + PubKey: validatorAccount.PublicKey().Marshal(), + WithdrawalCredentials: withdrawalCredentials, + Value: val, + } + signingRoot, err := ssz.HashTreeRoot(depositData) + errCheck(err, "Failed to generate deposit data signing root") + outputIf(debug, fmt.Sprintf("Signing root is %x", signingRoot)) + domain := types.Domain(types.DomainDeposit, []byte{0, 0, 0, 0}) + err = validatorAccount.Unlock([]byte(rootAccountPassphrase)) + errCheck(err, "Failed to unlock validator account") + signature, err := validatorAccount.Sign(signingRoot[:], domain) + validatorAccount.Lock() + errCheck(err, "Failed to sign deposit data signing root") - signedDepositData := struct { - PubKey []byte `ssz-size:"48"` - WithdrawalCredentials []byte `ssz-size:"32"` - Value uint64 - Signature []byte `ssz-size:"96"` - }{ - PubKey: validatorAccount.PublicKey().Marshal(), - WithdrawalCredentials: withdrawalCredentials, - Value: val, - Signature: signature.Marshal(), + signedDepositData := struct { + PubKey []byte `ssz-size:"48"` + WithdrawalCredentials []byte `ssz-size:"32"` + Value uint64 + Signature []byte `ssz-size:"96"` + }{ + PubKey: validatorAccount.PublicKey().Marshal(), + WithdrawalCredentials: withdrawalCredentials, + Value: val, + Signature: signature.Marshal(), + } + + outputIf(debug, fmt.Sprintf("Deposit data signature is %x", signedDepositData.Signature)) + + depositDataRoot, err := ssz.HashTreeRoot(signedDepositData) + errCheck(err, "Failed to generate deposit data root") + outputIf(debug, fmt.Sprintf("Deposit data root is %x", depositDataRoot)) + + if validatorDepositDataRaw { + // Build a raw transaction by hand + txData := []byte{0x22, 0x89, 0x51, 0x18} + // Pointer to validator public key + txData = append(txData, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}...) + // Pointer to withdrawal credentials + txData = append(txData, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0}...) + // Pointer to validator signature + txData = append(txData, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20}...) + // Deposit data root + txData = append(txData, depositDataRoot[:]...) + // Validator public key (pad to 32-byte boundary) + txData = append(txData, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30}...) + txData = append(txData, validatorAccount.PublicKey().Marshal()...) + txData = append(txData, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}...) + // Withdrawal credentials + txData = append(txData, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20}...) + txData = append(txData, withdrawalCredentials...) + // Deposit signature + txData = append(txData, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60}...) + txData = append(txData, signedDepositData.Signature...) + outputs = append(outputs, fmt.Sprintf("%#x", txData)) + } else { + outputs = append(outputs, fmt.Sprintf(`{"account":"%s","pubkey":"%048x","withdrawal_credentials":"%032x","signature":"%096x","value":%d,"deposit_data_root":"%032x"}`, fmt.Sprintf("%s/%s", validatorWallet.Name(), validatorAccount.Name()), signedDepositData.PubKey, signedDepositData.WithdrawalCredentials, signedDepositData.Signature, val, depositDataRoot)) + } } - outputIf(debug, fmt.Sprintf("Deposit data signature is %x", signedDepositData.Signature)) + if quiet { + os.Exit(0) + } - depositDataRoot, err := ssz.HashTreeRoot(signedDepositData) - errCheck(err, "Failed to generate deposit data root") - outputIf(debug, fmt.Sprintf("Deposit data root is %x", depositDataRoot)) - - outputIf(!quiet, fmt.Sprintf(`{"pubkey":"%048x","withdrawal_credentials":"%032x","signature":"%096x","value":%d,"deposit_data_root":"%032x"}`, signedDepositData.PubKey, signedDepositData.WithdrawalCredentials, signedDepositData.Signature, val, depositDataRoot)) - os.Exit(0) + if len(outputs) == 1 { + fmt.Printf("%s\n", outputs[0]) + } else { + fmt.Printf("[") + fmt.Print(strings.Join(outputs, ",")) + fmt.Println("]") + } }, } @@ -104,4 +160,5 @@ func init() { validatorDepositDataCmd.Flags().StringVar(&validatorDepositDataValidatorAccount, "validatoraccount", "", "Account of the account carrying out the validation") validatorDepositDataCmd.Flags().StringVar(&validatorDepositDataWithdrawalAccount, "withdrawalaccount", "", "Account of the account to which the validator funds will be withdrawn") validatorDepositDataCmd.Flags().StringVar(&validatorDepositDataDepositValue, "depositvalue", "", "Value of the amount to be deposited") + validatorDepositDataCmd.Flags().BoolVar(&validatorDepositDataRaw, "raw", false, "Print raw deposit data transaction data") }