diff --git a/cmd/depositverify.go b/cmd/depositverify.go index ac91c92..55e12f7 100644 --- a/cmd/depositverify.go +++ b/cmd/depositverify.go @@ -1,4 +1,4 @@ -// Copyright © 2019, 2020 Weald Technology Trading +// Copyright © 2019-2021 Weald Technology Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -32,6 +32,7 @@ import ( var depositVerifyData string var depositVerifyWithdrawalPubKey string +var depositVerifyWithdrawalAddress string var depositVerifyValidatorPubKey string var depositVerifyDepositAmount string var depositVerifyForkVersion string @@ -81,7 +82,14 @@ In quiet mode this will return 0 if the the data is verified correctly, otherwis withdrawalPubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes) errCheck(err, "Value supplied with --withdrawalpubkey is not a valid public key") withdrawalCredentials = eth2util.SHA256(withdrawalPubKey.Marshal()) - withdrawalCredentials[0] = 0 // BLS_WITHDRAWAL_PREFIX + withdrawalCredentials[0] = 0x00 // BLS_WITHDRAWAL_PREFIX + } else if depositVerifyWithdrawalAddress != "" { + withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(depositVerifyWithdrawalAddress, "0x")) + errCheck(err, "Invalid withdrawal address") + assert(len(withdrawalAddressBytes) == 20, "address should be 20 bytes") + withdrawalCredentials = make([]byte, 32) + withdrawalCredentials[0] = 0x01 // ETH1_ADDRESS_WITHDRAWAL_PREFIX + copy(withdrawalCredentials[12:], withdrawalAddressBytes) } outputIf(debug, fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials)) @@ -181,10 +189,10 @@ func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) { func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, validatorPubKeys map[[48]byte]bool, amount uint64) (bool, error) { if withdrawalCredentials == nil { - outputIf(!quiet, "Withdrawal public key not supplied; withdrawal credentials NOT checked") + outputIf(!quiet, "Withdrawal public key or address not supplied; withdrawal credentials NOT checked") } else { if !bytes.Equal(deposit.WithdrawalCredentials, withdrawalCredentials) { - outputIf(!quiet, "Withdrawal public key incorrect") + outputIf(!quiet, "Withdrawal credentials incorrect") return false, nil } outputIf(!quiet, "Withdrawal credentials verified") @@ -263,6 +271,7 @@ func init() { depositFlags(depositVerifyCmd) depositVerifyCmd.Flags().StringVar(&depositVerifyData, "data", "", "JSON data, or path to JSON data") depositVerifyCmd.Flags().StringVar(&depositVerifyWithdrawalPubKey, "withdrawalpubkey", "", "Public key of the account to which the validator funds will be withdrawn") + depositVerifyCmd.Flags().StringVar(&depositVerifyWithdrawalAddress, "withdrawaladdress", "", "Ethereum 1 address of the account to which the validator funds will be withdrawn") depositVerifyCmd.Flags().StringVar(&depositVerifyDepositAmount, "depositvalue", "32 Ether", "Value of the amount to be deposited") depositVerifyCmd.Flags().StringVar(&depositVerifyValidatorPubKey, "validatorpubkey", "", "Public key(s) of the account(s) that will be carrying out validation") depositVerifyCmd.Flags().StringVar(&depositVerifyForkVersion, "forkversion", "0x00000000", "Fork version of the chain of the deposit") diff --git a/cmd/validator/depositdata/input.go b/cmd/validator/depositdata/input.go index d049b12..1c60846 100644 --- a/cmd/validator/depositdata/input.go +++ b/cmd/validator/depositdata/input.go @@ -17,25 +17,28 @@ import ( "context" "encoding/hex" "strings" + "time" spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" "github.com/spf13/viper" ethdoutil "github.com/wealdtech/ethdo/util" e2types "github.com/wealdtech/go-eth2-types/v2" - util "github.com/wealdtech/go-eth2-util" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" string2eth "github.com/wealdtech/go-string2eth" ) type dataIn struct { - format string - withdrawalCredentials []byte - amount spec.Gwei - validatorAccounts []e2wtypes.Account - forkVersion *spec.Version - domain *spec.Domain - passphrases []string + format string + timeout time.Duration + withdrawalAccount string + withdrawalPubKey string + withdrawalAddress string + amount spec.Gwei + validatorAccounts []e2wtypes.Account + forkVersion *spec.Version + domain *spec.Domain + passphrases []string } func input() (*dataIn, error) { @@ -49,6 +52,11 @@ func input() (*dataIn, error) { return nil, errors.New("validator account is required") } + if viper.GetDuration("timeout") == 0 { + return nil, errors.New("timeout is required") + } + data.timeout = viper.GetDuration("timeout") + ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) defer cancel() _, data.validatorAccounts, err = ethdoutil.WalletAndAccountsFromPath(ctx, viper.GetString("validatoraccount")) @@ -70,37 +78,25 @@ func input() (*dataIn, error) { data.passphrases = ethdoutil.GetPassphrases() - switch { - case viper.GetString("withdrawalaccount") != "": - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - _, withdrawalAccount, err := ethdoutil.WalletAndAccountFromPath(ctx, viper.GetString("withdrawalaccount")) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain withdrawal account") - } - pubKey, err := ethdoutil.BestPublicKey(withdrawalAccount) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain public key for withdrawal account") - } - data.withdrawalCredentials = util.SHA256(pubKey.Marshal()) - case viper.GetString("withdrawalpubkey") != "": - withdrawalPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("withdrawalpubkey"), "0x")) - if err != nil { - return nil, errors.Wrap(err, "failed to decode withdrawal public key") - } - if len(withdrawalPubKeyBytes) != 48 { - return nil, errors.New("withdrawal public key must be exactly 48 bytes in length") - } - withdrawalPubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes) - if err != nil { - return nil, errors.Wrap(err, "withdrawal public key is not valid") - } - data.withdrawalCredentials = util.SHA256(withdrawalPubKey.Marshal()) - default: - return nil, errors.New("withdrawalaccount or withdrawal public key is required") + data.withdrawalAccount = viper.GetString("withdrawalaccount") + data.withdrawalPubKey = viper.GetString("withdrawalpubkey") + data.withdrawalAddress = viper.GetString("withdrawaladdress") + withdrawalDetailsPresent := 0 + if data.withdrawalAccount != "" { + withdrawalDetailsPresent++ + } + if data.withdrawalPubKey != "" { + withdrawalDetailsPresent++ + } + if data.withdrawalAddress != "" { + withdrawalDetailsPresent++ + } + if withdrawalDetailsPresent == 0 { + return nil, errors.New("withdrawal account, public key or address is required") + } + if withdrawalDetailsPresent > 1 { + return nil, errors.New("only one of withdrawal account, public key or address is allowed") } - // This is hard-coded, to allow deposit data to be generated without a connection to the beacon node. - data.withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX if viper.GetString("depositvalue") == "" { return nil, errors.New("deposit value is required") @@ -135,7 +131,7 @@ func inputForkVersion(ctx context.Context) (*spec.Version, error) { if err != nil { return nil, errors.Wrap(err, "failed to decode fork version") } - if len(forkVersion) != 4 { + if len(data) != 4 { return nil, errors.New("fork version must be exactly 4 bytes in length") } diff --git a/cmd/validator/depositdata/input_internal_test.go b/cmd/validator/depositdata/input_internal_test.go index 3aee421..432a0ef 100644 --- a/cmd/validator/depositdata/input_internal_test.go +++ b/cmd/validator/depositdata/input_internal_test.go @@ -1,4 +1,4 @@ -// Copyright © 2019, 2020 Weald Technology Trading +// Copyright © 2019-2021 Weald Technology Trading // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -84,9 +84,20 @@ func TestInput(t *testing.T) { name: "Nil", err: "validator account is required", }, + { + name: "TimeoutMissing", + vars: map[string]interface{}{ + "validatoraccount": "Test/Interop 0", + "withdrawalaccount": "Test/Interop 0", + "depositvalue": "32 Ether", + "forkversion": "0x01020304", + }, + err: "timeout is required", + }, { name: "ValidatorAccountMissing", vars: map[string]interface{}{ + "timeout": "10s", "withdrawalaccount": "Test/Interop 0", "depositvalue": "32 Ether", "forkversion": "0x01020304", @@ -96,6 +107,7 @@ func TestInput(t *testing.T) { { name: "ValidatorAccountUnknown", vars: map[string]interface{}{ + "timeout": "10s", "validatoraccount": "Test/Unknown", "withdrawalaccount": "Test/Interop 0", "depositvalue": "32 Ether", @@ -104,59 +116,74 @@ func TestInput(t *testing.T) { err: "unknown validator account", }, { - name: "WithdrawalAccountMissing", + name: "WithdrawalDetailsMissing", vars: map[string]interface{}{ + "timeout": "10s", "launchpad": true, "validatoraccount": "Test/Interop 0", "depositvalue": "32 Ether", "forkversion": "0x01020304", }, - err: "withdrawalaccount or withdrawal public key is required", + err: "withdrawal account, public key or address is required", }, { - name: "WithdrawalAccountUnknown", + name: "WithdrawalDetailsTooMany1", vars: map[string]interface{}{ - "raw": true, + "timeout": "10s", + "launchpad": true, "validatoraccount": "Test/Interop 0", - "withdrawalaccount": "Test/Unknown", + "withdrawalaccount": "Test/Interop 0", + "withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", "depositvalue": "32 Ether", "forkversion": "0x01020304", }, - err: "failed to obtain withdrawal account: failed to obtain account: no account with name \"Unknown\"", + err: "only one of withdrawal account, public key or address is allowed", }, { - name: "WithdrawalPubKeyInvalid", + name: "WithdrawalDetailsTooMany2", vars: map[string]interface{}{ - "validatoraccount": "Test/Interop 0", - "withdrawalpubkey": "invalid", - "depositvalue": "32 Ether", - "forkversion": "0x01020304", + "timeout": "10s", + "launchpad": true, + "validatoraccount": "Test/Interop 0", + "withdrawalaccount": "Test/Interop 0", + "withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "withdrawaladdress": "0x30C99930617B7b793beaB603ecEB08691005f2E5", + "depositvalue": "32 Ether", + "forkversion": "0x01020304", }, - err: "failed to decode withdrawal public key: encoding/hex: invalid byte: U+0069 'i'", + err: "only one of withdrawal account, public key or address is allowed", }, { - name: "WithdrawalPubKeyWrongLength", + name: "WithdrawalDetailsTooMany3", vars: map[string]interface{}{ - "validatoraccount": "Test/Interop 0", - "withdrawalpubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0bff", - "depositvalue": "32 Ether", - "forkversion": "0x01020304", + "timeout": "10s", + "launchpad": true, + "validatoraccount": "Test/Interop 0", + "withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "withdrawaladdress": "0x30C99930617B7b793beaB603ecEB08691005f2E5", + "depositvalue": "32 Ether", + "forkversion": "0x01020304", }, - err: "withdrawal public key must be exactly 48 bytes in length", + err: "only one of withdrawal account, public key or address is allowed", }, { - name: "WithdrawalPubKeyNotPubKey", + name: "WithdrawalDetailsTooMany4", vars: map[string]interface{}{ - "validatoraccount": "Test/Interop 0", - "withdrawalpubkey": "0x089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", - "depositvalue": "32 Ether", - "forkversion": "0x01020304", + "timeout": "10s", + "launchpad": true, + "validatoraccount": "Test/Interop 0", + "withdrawalaccount": "Test/Interop 0", + "withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "withdrawaladdress": "0x30C99930617B7b793beaB603ecEB08691005f2E5", + "depositvalue": "32 Ether", + "forkversion": "0x01020304", }, - err: "withdrawal public key is not valid: failed to deserialize public key: err blsPublicKeyDeserialize 089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + err: "only one of withdrawal account, public key or address is allowed", }, { name: "DepositValueMissing", vars: map[string]interface{}{ + "timeout": "10s", "validatoraccount": "Test/Interop 0", "withdrawalaccount": "Test/Interop 0", "forkversion": "0x01020304", @@ -166,6 +193,7 @@ func TestInput(t *testing.T) { { name: "DepositValueTooSmall", vars: map[string]interface{}{ + "timeout": "10s", "validatoraccount": "Test/Interop 0", "withdrawalaccount": "Test/Interop 0", "depositvalue": "1000 Wei", @@ -176,6 +204,7 @@ func TestInput(t *testing.T) { { name: "DepositValueInvalid", vars: map[string]interface{}{ + "timeout": "10s", "validatoraccount": "Test/Interop 0", "withdrawalaccount": "Test/Interop 0", "depositvalue": "1 groat", @@ -186,6 +215,7 @@ func TestInput(t *testing.T) { { name: "ForkVersionInvalid", vars: map[string]interface{}{ + "timeout": "10s", "validatoraccount": "Test/Interop 0", "withdrawalaccount": "Test/Interop 0", "depositvalue": "32 Ether", @@ -193,54 +223,68 @@ func TestInput(t *testing.T) { }, err: "failed to obtain fork version: failed to decode fork version: encoding/hex: invalid byte: U+0069 'i'", }, + { + name: "ForkVersionShort", + vars: map[string]interface{}{ + "timeout": "10s", + "validatoraccount": "Test/Interop 0", + "withdrawalaccount": "Test/Interop 0", + "depositvalue": "32 Ether", + "forkversion": "0x01", + }, + err: "failed to obtain fork version: fork version must be exactly 4 bytes in length", + }, { name: "Good", vars: map[string]interface{}{ + "timeout": "10s", "validatoraccount": "Test/Interop 0", "withdrawalaccount": "Test/Interop 0", "depositvalue": "32 Ether", }, res: &dataIn{ - format: "json", - withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - amount: 32000000000, - validatorAccounts: []e2wtypes.Account{interop0}, - forkVersion: mainnetForkVersion, - domain: mainnetDomain, + format: "json", + withdrawalAccount: "Test/Interop 0", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: mainnetForkVersion, + domain: mainnetDomain, }, }, { name: "GoodForkVersionOverride", vars: map[string]interface{}{ + "timeout": "10s", "validatoraccount": "Test/Interop 0", "withdrawalaccount": "Test/Interop 0", "depositvalue": "32 Ether", "forkversion": "0x01020304", }, res: &dataIn{ - format: "json", - withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - amount: 32000000000, - validatorAccounts: []e2wtypes.Account{interop0}, - forkVersion: forkVersion, - domain: domain, + format: "json", + withdrawalAccount: "Test/Interop 0", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, }, }, { name: "GoodWithdrawalPubKey", vars: map[string]interface{}{ + "timeout": "10s", "validatoraccount": "Test/Interop 0", "withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", "depositvalue": "32 Ether", "forkversion": "0x01020304", }, res: &dataIn{ - format: "json", - withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - amount: 32000000000, - validatorAccounts: []e2wtypes.Account{interop0}, - forkVersion: forkVersion, - domain: domain, + format: "json", + withdrawalPubKey: "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, }, }, } @@ -258,7 +302,9 @@ func TestInput(t *testing.T) { require.NoError(t, err) // Cannot compare accounts directly, so need to check each element individually. require.Equal(t, test.res.format, res.format) - require.Equal(t, test.res.withdrawalCredentials, res.withdrawalCredentials) + require.Equal(t, test.res.withdrawalAccount, res.withdrawalAccount) + require.Equal(t, test.res.withdrawalAddress, res.withdrawalAddress) + require.Equal(t, test.res.withdrawalPubKey, res.withdrawalPubKey) require.Equal(t, test.res.amount, res.amount) require.Equal(t, test.res.forkVersion, res.forkVersion) require.Equal(t, test.res.domain, res.domain) diff --git a/cmd/validator/depositdata/process.go b/cmd/validator/depositdata/process.go index b2c366c..23b6241 100644 --- a/cmd/validator/depositdata/process.go +++ b/cmd/validator/depositdata/process.go @@ -1,4 +1,4 @@ -// Copyright © 2019, 2020 Weald Technology Trading +// Copyright © 2019-2021 Weald Technology Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -15,12 +15,16 @@ package depositdata import ( "context" + "encoding/hex" "fmt" + "strings" spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" "github.com/wealdtech/ethdo/signing" - "github.com/wealdtech/ethdo/util" + ethdoutil "github.com/wealdtech/ethdo/util" + e2types "github.com/wealdtech/go-eth2-types/v2" + util "github.com/wealdtech/go-eth2-util" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) @@ -31,8 +35,13 @@ func process(data *dataIn) ([]*dataOut, error) { results := make([]*dataOut, 0) + withdrawalCredentials, err := createWithdrawalCredentials(data) + if err != nil { + return nil, err + } + for _, validatorAccount := range data.validatorAccounts { - validatorPubKey, err := util.BestPublicKey(validatorAccount) + validatorPubKey, err := ethdoutil.BestPublicKey(validatorAccount) if err != nil { return nil, errors.Wrap(err, "validator account does not provide a public key") } @@ -41,7 +50,7 @@ func process(data *dataIn) ([]*dataOut, error) { copy(pubKey[:], validatorPubKey.Marshal()) depositMessage := &spec.DepositMessage{ PublicKey: pubKey, - WithdrawalCredentials: data.withdrawalCredentials, + WithdrawalCredentials: withdrawalCredentials, Amount: data.amount, } root, err := depositMessage.HashTreeRoot() @@ -58,7 +67,7 @@ func process(data *dataIn) ([]*dataOut, error) { depositData := &spec.DepositData{ PublicKey: pubKey, - WithdrawalCredentials: data.withdrawalCredentials, + WithdrawalCredentials: withdrawalCredentials, Amount: data.amount, Signature: sig, } @@ -75,7 +84,7 @@ func process(data *dataIn) ([]*dataOut, error) { format: data.format, account: fmt.Sprintf("%s/%s", validatorWallet.Name(), validatorAccount.Name()), validatorPubKey: &pubKey, - withdrawalCredentials: data.withdrawalCredentials, + withdrawalCredentials: withdrawalCredentials, amount: data.amount, signature: &sig, forkVersion: data.forkVersion, @@ -85,3 +94,80 @@ func process(data *dataIn) ([]*dataOut, error) { } return results, nil } + +// createWithdrawalCredentials creates withdrawal credentials given an account, public key or Ethereum 1 address. +func createWithdrawalCredentials(data *dataIn) ([]byte, error) { + var withdrawalCredentials []byte + + switch { + case data.withdrawalAccount != "": + ctx, cancel := context.WithTimeout(context.Background(), data.timeout) + defer cancel() + _, withdrawalAccount, err := ethdoutil.WalletAndAccountFromPath(ctx, data.withdrawalAccount) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain withdrawal account") + } + pubKey, err := ethdoutil.BestPublicKey(withdrawalAccount) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain public key for withdrawal account") + } + withdrawalCredentials = util.SHA256(pubKey.Marshal()) + // This is hard-coded, to allow deposit data to be generated without a connection to the beacon node. + withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX + case data.withdrawalPubKey != "": + withdrawalPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalPubKey, "0x")) + if err != nil { + return nil, errors.Wrap(err, "failed to decode withdrawal public key") + } + if len(withdrawalPubKeyBytes) != 48 { + return nil, errors.New("withdrawal public key must be exactly 48 bytes in length") + } + pubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes) + if err != nil { + return nil, errors.Wrap(err, "withdrawal public key is not valid") + } + withdrawalCredentials = util.SHA256(pubKey.Marshal()) + // This is hard-coded, to allow deposit data to be generated without a connection to the beacon node. + withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX + case data.withdrawalAddress != "": + withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalAddress, "0x")) + if err != nil { + return nil, errors.Wrap(err, "failed to decode withdrawal address") + } + if len(withdrawalAddressBytes) != 20 { + return nil, errors.New("withdrawal address must be exactly 20 bytes in length") + } + // Ensure the address is properly checksummed. + checksummedAddress := addressBytesToEIP55(withdrawalAddressBytes) + if checksummedAddress != data.withdrawalAddress { + return nil, fmt.Errorf("withdrawal address checksum does not match (expected %s)", checksummedAddress) + } + withdrawalCredentials = make([]byte, 32) + copy(withdrawalCredentials[12:32], withdrawalAddressBytes) + // This is hard-coded, to allow deposit data to be generated without a connection to the beacon node. + withdrawalCredentials[0] = byte(1) // ETH1_ADDRESS_WITHDRAWAL_PREFIX + default: + return nil, errors.New("withdrawal account, public key or address is required") + } + + return withdrawalCredentials, nil +} + +// addressBytesToEIP55 converts a byte array in to an EIP-55 string format. +func addressBytesToEIP55(address []byte) string { + bytes := []byte(fmt.Sprintf("%x", address)) + hash := util.Keccak256(bytes) + for i := 0; i < len(bytes); i++ { + hashByte := hash[i/2] + if i%2 == 0 { + hashByte >>= 4 + } else { + hashByte &= 0xf + } + if bytes[i] > '9' && hashByte > 7 { + bytes[i] -= 32 + } + } + + return fmt.Sprintf("0x%s", string(bytes)) +} diff --git a/cmd/validator/depositdata/process_internal_test.go b/cmd/validator/depositdata/process_internal_test.go index 3503873..d162c93 100644 --- a/cmd/validator/depositdata/process_internal_test.go +++ b/cmd/validator/depositdata/process_internal_test.go @@ -1,4 +1,4 @@ -// Copyright © 2019, 2020 eald Technology Trading +// Copyright © 2019-2021 Weald Technology Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -15,6 +15,8 @@ package depositdata import ( "context" + "encoding/hex" + "strings" "testing" spec "github.com/attestantio/go-eth2-client/spec/phase0" @@ -49,6 +51,10 @@ func TestProcess(t *testing.T) { ) require.NoError(t, err) + withdrawalAccount := "Test/Interop 0" + withdrawalPubKey := "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c" + withdrawalAddress := "0x30C99930617B7b793beaB603ecEB08691005f2E5" + var validatorPubKey *spec.BLSPubKey { tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c") @@ -101,6 +107,22 @@ func TestProcess(t *testing.T) { depositMessageRoot2 = &tmp } + var depositDataRoot3 *spec.Root + { + tmp := testutil.HexToRoot("0x489500535b03dd9deffa0f00cb38d82346111856fb58a9541fe1f01a1a97429c") + depositDataRoot3 = &tmp + } + var depositMessageRoot3 *spec.Root + { + tmp := testutil.HexToRoot("0x7b8ee5694e4338cf2bfe5a4d2f46540f0ade85ebd30713673cf5783c4e925681") + depositMessageRoot3 = &tmp + } + var signature3 *spec.BLSSignature + { + tmp := testutil.HexToSignature("0xba0019d5c421f205d845782f52a87ab95cd489fbef2911f8a1f9cf7c14b4ce59eefa82641e770a4cb405534b7776d0f801b0a8b178c1b71b718c104e89f4e633da10a398c7919a00c403d58f3f4b827af8adb263b192e7a45b0ed1926dff5f66") + signature3 = &tmp + } + tests := []struct { name string dataIn *dataIn @@ -111,16 +133,119 @@ func TestProcess(t *testing.T) { name: "Nil", err: "no data", }, + { + name: "WithdrawalDetailsMissing", + dataIn: &dataIn{ + format: "raw", + passphrases: []string{"pass"}, + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, + }, + err: "withdrawal account, public key or address is required", + }, + { + name: "WithdrawalAccountUnknown", + dataIn: &dataIn{ + format: "raw", + passphrases: []string{"pass"}, + withdrawalAccount: "Unknown", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, + }, + err: "failed to obtain withdrawal account: failed to open wallet for account: wallet not found", + }, + { + name: "WithdrawalPubKeyInvalid", + dataIn: &dataIn{ + format: "raw", + passphrases: []string{"pass"}, + withdrawalPubKey: "invalid", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, + }, + err: "failed to decode withdrawal public key: encoding/hex: invalid byte: U+0069 'i'", + }, + { + name: "WithdrawalPubKeyWrongLength", + dataIn: &dataIn{ + format: "raw", + passphrases: []string{"pass"}, + withdrawalPubKey: "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0bff", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, + }, + err: "withdrawal public key must be exactly 48 bytes in length", + }, + { + name: "WithdrawalPubKeyNotPubKey", + dataIn: &dataIn{ + format: "raw", + passphrases: []string{"pass"}, + withdrawalPubKey: "0x089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, + }, + err: "withdrawal public key is not valid: failed to deserialize public key: err blsPublicKeyDeserialize 089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + }, + { + name: "WithdrawalAddressInvalid", + dataIn: &dataIn{ + format: "raw", + passphrases: []string{"pass"}, + withdrawalAddress: "invalid", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, + }, + err: "failed to decode withdrawal address: encoding/hex: invalid byte: U+0069 'i'", + }, + { + name: "WithdrawalAddressWrongLength", + dataIn: &dataIn{ + format: "raw", + passphrases: []string{"pass"}, + withdrawalAddress: "0x30C99930617B7b793beaB603ecEB08691005f2", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, + }, + err: "withdrawal address must be exactly 20 bytes in length", + }, + { + name: "WithdrawalAddressIncorrectChecksum", + dataIn: &dataIn{ + format: "raw", + passphrases: []string{"pass"}, + withdrawalAddress: "0x30c99930617b7b793beab603eceb08691005f2e5", + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, + }, + err: "withdrawal address checksum does not match (expected 0x30C99930617B7b793beaB603ecEB08691005f2E5)", + }, { name: "Single", dataIn: &dataIn{ - format: "raw", - passphrases: []string{"pass"}, - withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - amount: 32000000000, - validatorAccounts: []e2wtypes.Account{interop0}, - forkVersion: forkVersion, - domain: domain, + format: "raw", + passphrases: []string{"pass"}, + withdrawalAccount: withdrawalAccount, + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, }, res: []*dataOut{ { @@ -139,13 +264,13 @@ func TestProcess(t *testing.T) { { name: "Double", dataIn: &dataIn{ - format: "raw", - passphrases: []string{"pass"}, - withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - amount: 32000000000, - validatorAccounts: []e2wtypes.Account{interop0, interop1}, - forkVersion: forkVersion, - domain: domain, + format: "raw", + passphrases: []string{"pass"}, + withdrawalPubKey: withdrawalPubKey, + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0, interop1}, + forkVersion: forkVersion, + domain: domain, }, res: []*dataOut{ { @@ -172,6 +297,31 @@ func TestProcess(t *testing.T) { }, }, }, + { + name: "WithdrawalAddress", + dataIn: &dataIn{ + format: "raw", + passphrases: []string{"pass"}, + withdrawalAddress: withdrawalAddress, + amount: 32000000000, + validatorAccounts: []e2wtypes.Account{interop0}, + forkVersion: forkVersion, + domain: domain, + }, + res: []*dataOut{ + { + format: "raw", + account: "Test/Interop 0", + validatorPubKey: validatorPubKey, + amount: 32000000000, + withdrawalCredentials: testutil.HexToBytes("0x01000000000000000000000030C99930617B7b793beaB603ecEB08691005f2E5"), + signature: signature3, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot3, + depositMessageRoot: depositMessageRoot3, + }, + }, + }, } for _, test := range tests { @@ -186,3 +336,18 @@ func TestProcess(t *testing.T) { }) } } + +func TestAddressBytesToEIP55(t *testing.T) { + tests := []string{ + "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", + "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", + "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", + "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", + } + + for _, test := range tests { + bytes, err := hex.DecodeString(strings.TrimPrefix(test, "0x")) + require.NoError(t, err) + require.Equal(t, addressBytesToEIP55(bytes), test) + } +} diff --git a/cmd/validatordepositdata.go b/cmd/validatordepositdata.go index 542c5a8..1c50ae0 100644 --- a/cmd/validatordepositdata.go +++ b/cmd/validatordepositdata.go @@ -49,9 +49,10 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth func init() { validatorCmd.AddCommand(validatorDepositDataCmd) validatorFlags(validatorDepositDataCmd) - validatorDepositDataCmd.Flags().String("validatoraccount", "", "Account of the account carrying out the validation") - validatorDepositDataCmd.Flags().String("withdrawalaccount", "", "Account of the account to which the validator funds will be withdrawn") + validatorDepositDataCmd.Flags().String("validatoraccount", "", "Account carrying out the validation") + validatorDepositDataCmd.Flags().String("withdrawalaccount", "", "Account to which the validator funds will be withdrawn") validatorDepositDataCmd.Flags().String("withdrawalpubkey", "", "Public key of the account to which the validator funds will be withdrawn") + validatorDepositDataCmd.Flags().String("withdrawaladdress", "", "Ethereum 1 address of the account to which the validator funds will be withdrawn") validatorDepositDataCmd.Flags().String("depositvalue", "", "Value of the amount to be deposited") validatorDepositDataCmd.Flags().Bool("raw", false, "Print raw deposit data transaction data") validatorDepositDataCmd.Flags().String("forkversion", "", "Use a hard-coded fork version (default is to fetch it from the node)") @@ -68,6 +69,9 @@ func validatorDepositdataBindings() { if err := viper.BindPFlag("withdrawalpubkey", validatorDepositDataCmd.Flags().Lookup("withdrawalpubkey")); err != nil { panic(err) } + if err := viper.BindPFlag("withdrawaladdress", validatorDepositDataCmd.Flags().Lookup("withdrawaladdress")); err != nil { + panic(err) + } if err := viper.BindPFlag("depositvalue", validatorDepositDataCmd.Flags().Lookup("depositvalue")); err != nil { panic(err) } diff --git a/go.mod b/go.mod index ad0e565..1c61d77 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/wealdtech/go-eth2-wallet-store-scratch v1.6.2 github.com/wealdtech/go-eth2-wallet-types/v2 v2.8.2 github.com/wealdtech/go-string2eth v1.1.0 + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/text v0.3.5 google.golang.org/genproto v0.0.0-20210201184850-646a494a81ea // indirect