Compare commits

...

8 Commits

Author SHA1 Message Date
Jim McDonald
b76cdb01d1 Update version. 2021-05-13 12:42:14 +01:00
Jim McDonald
ce5b250ef0 Report on missing interfaces.
This update handles the situation where an ETH2 client does not provide
all required interfaces for the 'chain status' command, returning an
error rather than simply panicing.

Fixes #35.
2021-05-13 12:39:14 +01:00
Jim McDonald
2c4ccf62af Avoid crash with latest version of herumi/go-bls. 2021-05-13 12:37:46 +01:00
Jim McDonald
c7ad5194e6 Bump version number. 2021-03-14 22:03:22 +00:00
Jim McDonald
ddb866131b Merge pull request #32 from wealdtech/eth1-withdrawal-credentials
Allow use of Ethereum 1 withdrawal credentials
2021-03-14 21:47:21 +00:00
Jim McDonald
49fb03aa3a Allow use of Ethereum 1 withdrawal credentials.
Release 1.0.1 of the Ethereum 2 specification allows withdrawal
credentials to be Ethereum 1 addresses.  This enables the use of such
addresses when generating and verifying deposit data.
2021-03-12 12:53:42 +00:00
Jim McDonald
1ed3a51117 ETH1 withdrawal credentials. 2021-02-26 15:19:37 +00:00
Jim McDonald
4d5660ccbb Fix crash in attester/duties and inclusion.
A recent change for a return value going from an array to a map caused a
bad indexing in to the returned data.  This ensures that the value is
read directly from the map rather than using a hard-coded offset.
2021-02-13 22:25:26 +00:00
13 changed files with 450 additions and 118 deletions

View File

@@ -1,3 +1,14 @@
1.9.1
- Avoid crash when required interfaces for chain status command are not supported
- Avoid crash with latest version of herumi/go-bls
1.9.0
- allow use of Ethereum 1 address as withdrawal credentials
1.8.1
- fix issue where 'attester duties' and 'attester inclusion' could crash
1.8.0
- add "chain time"
- add "validator keycheck"

View File

@@ -66,7 +66,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
validator := validators[0]
var validator *api.Validator
for _, v := range validators {
validator = v
}
results := &dataOut{
debug: data.debug,

View File

@@ -66,7 +66,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
validator := validators[0]
var validator *api.Validator
for _, v := range validators {
validator = v
}
results := &dataOut{
debug: data.debug,

View File

@@ -40,13 +40,19 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
specProvider, isProvider := eth2Client.(eth2client.SpecProvider)
assert(isProvider, "beacon node does not provide spec; cannot report on chain status")
config, err := specProvider.Spec(ctx)
errCheck(err, "Failed to obtain beacon chain specification")
finality, err := eth2Client.(eth2client.FinalityProvider).Finality(ctx, "head")
finalityProvider, isProvider := eth2Client.(eth2client.FinalityProvider)
assert(isProvider, "beacon node does not provide finality; cannot report on chain status")
finality, err := finalityProvider.Finality(ctx, "head")
errCheck(err, "Failed to obtain finality information")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
genesisProvider, isProvider := eth2Client.(eth2client.GenesisProvider)
assert(isProvider, "beacon node does not provide genesis; cannot report on chain status")
genesis, err := genesisProvider.Genesis(ctx)
errCheck(err, "Failed to obtain genesis information")
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)

View File

@@ -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")

View File

@@ -70,7 +70,9 @@ In quiet mode this will return 0 if the the exit is verified correctly, otherwis
}
exitRoot, err := exit.HashTreeRoot()
errCheck(err, "Failed to obtain exit hash tree root")
sig, err := e2types.BLSSignatureFromBytes(data.Exit.Signature[:])
signatureBytes := make([]byte, 96)
copy(signatureBytes, data.Exit.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(signatureBytes)
errCheck(err, "Invalid signature")
verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig)
errCheck(err, "Failed to verify voluntary exit")

View File

@@ -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")
}

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -23,8 +23,8 @@ import (
)
// ReleaseVersion is the release version of the codebase.
// Usually overrideen by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.8.0)"
// Usually overridden by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.9.1)"
// versionCmd represents the version command
var versionCmd = &cobra.Command{

1
go.mod
View File

@@ -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