Move to eth2client

This commit is contained in:
Jim McDonald
2020-11-10 23:47:21 +00:00
parent 5a385c3c23
commit 93e632972a
56 changed files with 3533 additions and 1773 deletions

View File

@@ -36,7 +36,6 @@ func TestInput(t *testing.T) {
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",

View File

@@ -0,0 +1,130 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"encoding/hex"
"fmt"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/core"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
// Chain information.
slotsPerEpoch uint64
// Operation.
validator *api.Validator
eth2Client eth2client.Service
epoch spec.Epoch
account e2wtypes.Account
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Account.
var err error
data.account, err = attesterInclusionAccount()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
// Ethereum 2 client.
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
// Epoch
epoch := viper.GetInt64("epoch")
if epoch == -1 {
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis data")
}
epoch = int64(time.Since(genesis.GenesisTime).Seconds()) / (int64(slotDuration.Seconds()) * int64(data.slotsPerEpoch))
if epoch > 0 {
epoch--
}
}
data.epoch = spec.Epoch(epoch)
pubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := core.BestPublicKey(data.account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
}
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
data.validator = validators[0]
return data, nil
}
// attesterInclusionAccount obtains the account for the attester inclusion command.
func attesterInclusionAccount() (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, account, err = core.WalletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKey := viper.GetString("pubkey")
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", pubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", pubKey))
}
}
return account, nil
}

View File

@@ -0,0 +1,96 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "AccountMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to obtain account: invalid public key : public key must be 48 bytes",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
}
})
}
}

View File

@@ -0,0 +1,46 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"fmt"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
slot spec.Slot
attestationIndex uint64
inclusionDelay spec.Slot
found bool
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if !data.quiet {
if data.found {
return fmt.Sprintf("Attestation included in block %d, attestation %d (inclusion delay %d)", data.slot, data.attestationIndex, data.inclusionDelay), nil
}
return "Attestation not found", nil
}
return "", nil
}

View File

@@ -0,0 +1,62 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Empty",
dataOut: &dataOut{},
res: "Attestation not found",
},
{
name: "Found",
dataOut: &dataOut{
found: true,
slot: 123,
attestationIndex: 456,
inclusionDelay: 7,
},
res: "Attestation included in block 123, attestation 456 (inclusion delay 7)",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

View File

@@ -0,0 +1,85 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
duty, err := duty(ctx, data.eth2Client, data.validator, data.epoch, data.slotsPerEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain duty for validator")
}
startSlot := duty.Slot + 1
endSlot := startSlot + 32
for slot := startSlot; slot < endSlot; slot++ {
signedBlock, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block")
}
if signedBlock == nil {
continue
}
if signedBlock.Message.Slot != slot {
continue
}
if data.debug {
fmt.Printf("Fetched block for slot %d\n", slot)
}
for i, attestation := range signedBlock.Message.Body.Attestations {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
results.slot = slot
results.attestationIndex = uint64(i)
results.inclusionDelay = slot - duty.Slot
results.found = true
return results, nil
}
}
}
return nil, errors.New("not found")
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attester duties")
}
if len(duties) == 0 {
return nil, errors.New("validator does not have duty for that epoch")
}
return duties[0], nil
}

View File

@@ -0,0 +1,69 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"os"
"testing"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/auto"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
eth2Client, err := auto.New(context.Background(),
auto.WithLogLevel(zerolog.Disabled),
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
)
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Client",
dataIn: &dataIn{
eth2Client: eth2Client,
slotsPerEpoch: 32,
validator: &api.Validator{
Index: 0,
},
epoch: 100,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the wallet create data command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

@@ -14,19 +14,11 @@
package cmd
import (
"context"
"encoding/hex"
"fmt"
"os"
"strings"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/grpc"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
attesterinclusion "github.com/wealdtech/ethdo/cmd/attester/inclusion"
)
var attesterInclusionCmd = &cobra.Command{
@@ -37,111 +29,21 @@ var attesterInclusionCmd = &cobra.Command{
ethdo attester inclusion --account=Validators/00001 --epoch=12345
In quiet mode this will return 0 if an attestation from the attester is found on the block of the given epoch, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
err := connect()
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain block")
// Obtain the epoch.
epoch := viper.GetInt64("epoch")
if epoch == -1 {
outputIf(debug, "No epoch supplied; fetching current epoch")
config, err := grpc.FetchChainConfig(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain configuration")
slotsPerEpoch := config["SlotsPerEpoch"].(uint64)
secondsPerSlot := config["SecondsPerSlot"].(uint64)
genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain genesis")
epoch = int64(time.Since(genesisTime).Seconds()) / int64(secondsPerSlot*slotsPerEpoch)
if epoch > 0 {
epoch--
}
RunE: func(cmd *cobra.Command, args []string) error {
res, err := attesterinclusion.Run(cmd)
if err != nil {
return err
}
outputIf(debug, fmt.Sprintf("Epoch is %d", epoch))
// Obtain the validator.
account, err := attesterInclusionAccount()
errCheck(err, "Failed to obtain account")
validatorIndex, err := grpc.FetchValidatorIndex(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator")
// Find the attesting slot for the given epoch.
committees, err := grpc.FetchValidatorCommittees(eth2GRPCConn, uint64(epoch))
errCheck(err, "Failed to obtain validator committees")
slot := uint64(0)
committeeIndex := uint64(0)
validatorPositionInCommittee := uint64(0)
found := false
for searchSlot, committee := range committees {
for searchCommitteeIndex, committeeValidatorIndices := range committee {
for position, committeeValidatorIndex := range committeeValidatorIndices {
if validatorIndex == committeeValidatorIndex {
outputIf(verbose, fmt.Sprintf("Validator %d scheduled to attest at slot %d for epoch %d: entry %d in committee %d", validatorIndex, searchSlot, epoch, position, searchCommitteeIndex))
slot = searchSlot
committeeIndex = uint64(searchCommitteeIndex)
validatorPositionInCommittee = uint64(position)
found = true
break
}
}
}
if viper.GetBool("quiet") {
return nil
}
assert(found, fmt.Sprintf("Failed to find attester duty for validator in epoch %d", epoch))
startSlot := slot + 1
endSlot := startSlot + 32
for curSlot := startSlot; curSlot < endSlot; curSlot++ {
signedBlock, err := grpc.FetchBlock(eth2GRPCConn, curSlot)
errCheck(err, "Failed to obtain block")
if signedBlock == nil {
outputIf(debug, fmt.Sprintf("No block at slot %d", curSlot))
continue
}
outputIf(debug, fmt.Sprintf("Fetched block %d", curSlot))
for i, attestation := range signedBlock.Block.Body.Attestations {
outputIf(debug, fmt.Sprintf("Attestation %d is for slot %d and committee %d", i, attestation.Data.Slot, attestation.Data.CommitteeIndex))
if attestation.Data.Slot == slot &&
attestation.Data.CommitteeIndex == committeeIndex &&
attestation.AggregationBits.BitAt(validatorPositionInCommittee) {
if verbose {
fmt.Printf("Attestation for epoch %d included in block %d, attestation %d (inclusion delay %d)\n", epoch, curSlot, i, curSlot-slot)
} else if !quiet {
fmt.Printf("Attestation for epoch %d included in block %d (inclusion delay %d)\n", epoch, curSlot, curSlot-slot)
}
os.Exit(_exitSuccess)
}
}
if res != "" {
fmt.Println(res)
}
outputIf(verbose, fmt.Sprintf("Attestation for epoch %d not included on the chain", epoch))
os.Exit(_exitFailure)
return nil
},
}
// attesterInclusionAccount obtains the account for the attester inclusion command.
func attesterInclusionAccount() (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, account, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKey := viper.GetString("pubkey")
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", pubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", pubKey))
}
}
return account, nil
}
func init() {
attesterCmd.AddCommand(attesterInclusionCmd)
attesterFlags(attesterInclusionCmd)

68
cmd/block/info/input.go Normal file
View File

@@ -0,0 +1,68 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
// Operation.
eth2Client eth2client.Service
jsonOutput bool
// Chain information.
blockID string
stream bool
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
data.jsonOutput = viper.GetBool("json")
data.stream = viper.GetBool("stream")
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
if viper.GetString("blockid") == "" {
data.blockID = "head"
} else {
// Specific slot.
data.blockID = viper.GetString("blockid")
}
return data, nil
}

View File

@@ -0,0 +1,126 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"os"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
},
{
name: "ConnectionBad",
vars: map[string]interface{}{
"timeout": "5s",
"connection": "localhost:1",
"blockid": "justified",
},
res: &dataIn{
timeout: 5 * time.Second,
blockID: "justified",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
{
name: "BlockIDNil",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
res: &dataIn{
timeout: 5 * time.Second,
blockID: "head",
},
},
{
name: "BlockIDSpecific",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"blockid": "justified",
},
res: &dataIn{
timeout: 5 * time.Second,
blockID: "justified",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
require.Equal(t, test.res.blockID, res.blockID)
}
})
}
}

325
cmd/block/info/output.go Normal file
View File

@@ -0,0 +1,325 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"sort"
"strings"
"time"
"unicode/utf8"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
"github.com/wealdtech/go-string2eth"
)
type dataOut struct {
debug bool
verbose bool
eth2Client eth2client.Service
genesisTime time.Time
slotDuration time.Duration
slotsPerEpoch uint64
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
return "", nil
}
func outputBlockGeneral(ctx context.Context, verbose bool, block *spec.BeaconBlock, genesisTime time.Time, slotDuration time.Duration, slotsPerEpoch uint64) (string, error) {
bodyRoot, err := block.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate block root")
}
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Slot: %d\n", block.Slot))
res.WriteString(fmt.Sprintf("Epoch: %d\n", spec.Epoch(uint64(block.Slot)/slotsPerEpoch)))
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(block.Slot)*int64(slotDuration.Seconds()), 0)))
res.WriteString(fmt.Sprintf("Block root: %#x\n", bodyRoot))
if verbose {
res.WriteString(fmt.Sprintf("Parent root: %#x\n", block.ParentRoot))
res.WriteString(fmt.Sprintf("State root: %#x\n", block.StateRoot))
}
if len(block.Body.Graffiti) > 0 && hex.EncodeToString(block.Body.Graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
if utf8.Valid(block.Body.Graffiti) {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(block.Body.Graffiti)))
} else {
res.WriteString(fmt.Sprintf("Graffiti: %#x\n", block.Body.Graffiti))
}
}
return res.String(), nil
}
func outputBlockETH1Data(ctx context.Context, eth1Data *spec.ETH1Data) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Ethereum 1 deposit count: %d\n", eth1Data.DepositCount))
res.WriteString(fmt.Sprintf("Ethereum 1 deposit root: %#x\n", eth1Data.DepositRoot))
res.WriteString(fmt.Sprintf("Ethereum 1 block hash: %#x\n", eth1Data.BlockHash))
return res.String(), nil
}
func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*spec.Attestation) (string, error) {
res := strings.Builder{}
validatorCommittees := make(map[spec.Slot]map[spec.CommitteeIndex][]spec.ValidatorIndex)
res.WriteString(fmt.Sprintf("Attestations: %d\n", len(attestations)))
if verbose {
beaconCommitteesProvider, isProvider := eth2Client.(eth2client.BeaconCommitteesProvider)
if isProvider {
for i, att := range attestations {
res.WriteString(fmt.Sprintf(" %d:\n", i))
// Fetch committees for this epoch if not already obtained.
committees, exists := validatorCommittees[att.Data.Slot]
if !exists {
beaconCommittees, err := beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", att.Data.Slot))
if err != nil {
return "", errors.Wrap(err, "failed to obtain beacon committees")
}
for _, beaconCommittee := range beaconCommittees {
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
validatorCommittees[beaconCommittee.Slot] = make(map[spec.CommitteeIndex][]spec.ValidatorIndex)
}
validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
}
committees = validatorCommittees[att.Data.Slot]
}
res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index))
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitsToString(att.AggregationBits)))
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot))
res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot))
res.WriteString(fmt.Sprintf(" Source epoch: %d\n", att.Data.Source.Epoch))
res.WriteString(fmt.Sprintf(" Source root: %#x\n", att.Data.Source.Root))
res.WriteString(fmt.Sprintf(" Target epoch: %d\n", att.Data.Target.Epoch))
res.WriteString(fmt.Sprintf(" Target root: %#x\n", att.Data.Target.Root))
}
}
}
return res.String(), nil
}
func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*spec.AttesterSlashing) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings)))
if verbose {
for i, slashing := range attesterSlashings {
// Say what was slashed.
att1 := slashing.Attestation1
att2 := slashing.Attestation2
slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices)
if len(slashedIndices) == 0 {
continue
}
res.WriteString(fmt.Sprintf(" %d:\n", i))
res.WriteString(fmt.Sprintln(" Slashed validators:"))
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", slashedIndices)
if err != nil {
return "", errors.Wrap(err, "failed to obtain beacon committees")
}
for k, v := range validators {
res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k))
}
// Say what caused the slashing.
if att1.Data.Target.Epoch == att2.Data.Target.Epoch {
res.WriteString(fmt.Sprintf(" Double voted for same target epoch (%d):\n", att1.Data.Target.Epoch))
if !bytes.Equal(att1.Data.Target.Root[:], att2.Data.Target.Root[:]) {
res.WriteString(fmt.Sprintf(" Attestation 1 target epoch root: %#x\n", att1.Data.Target.Root))
res.WriteString(fmt.Sprintf(" Attestation 2target epoch root: %#x\n", att2.Data.Target.Root))
}
if !bytes.Equal(att1.Data.BeaconBlockRoot[:], att2.Data.BeaconBlockRoot[:]) {
res.WriteString(fmt.Sprintf(" Attestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot))
res.WriteString(fmt.Sprintf(" Attestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot))
}
} else if att1.Data.Source.Epoch < att2.Data.Source.Epoch &&
att1.Data.Target.Epoch > att2.Data.Target.Epoch {
res.WriteString(" Surround voted:\n")
res.WriteString(fmt.Sprintf(" Attestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch))
res.WriteString(fmt.Sprintf(" Attestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch))
}
}
}
return res.String(), nil
}
func outputBlockDeposits(ctx context.Context, verbose bool, deposits []*spec.Deposit) (string, error) {
res := strings.Builder{}
// Deposits.
res.WriteString(fmt.Sprintf("Deposits: %d\n", len(deposits)))
if verbose {
for i, deposit := range deposits {
data := deposit.Data
res.WriteString(fmt.Sprintf(" %d:\n", i))
res.WriteString(fmt.Sprintf(" Public key: %#x\n", data.PublicKey))
res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(data.Amount), true)))
res.WriteString(fmt.Sprintf(" Withdrawal credentials: %#x\n", data.WithdrawalCredentials))
res.WriteString(fmt.Sprintf(" Signature: %#x\n", data.Signature))
}
}
return res.String(), nil
}
func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Service, verbose bool, voluntaryExits []*spec.SignedVoluntaryExit) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Voluntary exits: %d\n", len(voluntaryExits)))
if verbose {
for i, voluntaryExit := range voluntaryExits {
res.WriteString(fmt.Sprintf(" %d:\n", i))
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []spec.ValidatorIndex{voluntaryExit.Message.ValidatorIndex})
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
} else {
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[0].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch))
}
}
}
return res.String(), nil
}
func outputBlockText(ctx context.Context, data *dataOut, signedBlock *spec.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
tmp, err := outputBlockGeneral(ctx, data.verbose, signedBlock.Message, data.genesisTime, data.slotDuration, data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// TODO verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
// intersection returns a list of items common between the two sets.
func intersection(set1 []uint64, set2 []uint64) []spec.ValidatorIndex {
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })
sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] })
res := make([]spec.ValidatorIndex, 0)
set1Pos := 0
set2Pos := 0
for set1Pos < len(set1) && set2Pos < len(set2) {
switch {
case set1[set1Pos] < set2[set2Pos]:
set1Pos++
case set2[set2Pos] < set1[set1Pos]:
set2Pos++
default:
res = append(res, spec.ValidatorIndex(set1[set1Pos]))
set1Pos++
set2Pos++
}
}
return res
}
func bitsToString(input bitfield.Bitlist) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s✓", res)
} else {
res = fmt.Sprintf("%s✕", res)
}
if i%8 == 7 {
res = fmt.Sprintf("%s ", res)
}
}
return strings.TrimSpace(res)
}
func attestingIndices(input bitfield.Bitlist, indices []spec.ValidatorIndex) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s%d ", res, indices[i])
}
}
return strings.TrimSpace(res)
}

View File

@@ -0,0 +1,177 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Good",
dataOut: &dataOut{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}
// func TestOutputBlockText(t *testing.T) {
// tests := []struct {
// name string
// dataOut *dataOut
// signedBeaconBlock *spec.SignedBeaconBlock
// err string
// }{
// {
// name: "Nil",
// err: "no data",
// },
// {
// name: "Good",
// dataOut: &dataOut{},
// },
// }
//
// for _, test := range tests {
// t.Run(test.name, func(t *testing.T) {
// res := outputBlockText(context.Background(), test.dataOut, test.signedBeaconBlock)
// if test.err != "" {
// require.EqualError(t, err, test.err)
// } else {
// require.NoError(t, err)
// require.Equal(t, test.res, res)
// }
// })
// }
// }
func TestOutputBlockDeposits(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
verbose bool
deposits []*spec.Deposit
res string
err string
}{
{
name: "Nil",
res: "Deposits: 0\n",
},
{
name: "Empty",
res: "Deposits: 0\n",
},
{
name: "Single",
deposits: []*spec.Deposit{
{
Data: &spec.DepositData{
PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
WithdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
Amount: spec.Gwei(32000000000),
Signature: testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
},
},
},
res: "Deposits: 1\n",
},
{
name: "SingleVerbose",
deposits: []*spec.Deposit{
{
Data: &spec.DepositData{
PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
WithdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
Amount: spec.Gwei(32000000000),
Signature: testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
},
},
},
verbose: true,
res: "Deposits: 1\n 0:\n Public key: 0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c\n Amount: 32 Ether\n Withdrawal credentials: 0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b\n Signature: 0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := outputBlockDeposits(context.Background(), test.verbose, test.deposits)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}
func TestOutputBlockETH1Data(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
verbose bool
eth1Data *spec.ETH1Data
res string
err string
}{
{
name: "Good",
eth1Data: &spec.ETH1Data{
DepositRoot: testutil.HexToRoot("0x92407b66d7daf4f30beb84820caae2cbba51add1c4648584101ff3c32151eb83"),
DepositCount: 109936,
BlockHash: testutil.HexToBytes("0x77b03ebaf0f2835b491cbd99a7f4649a03a6e7999678603030a014a3c48b32a4"),
},
res: "Ethereum 1 deposit count: 109936\nEthereum 1 deposit root: 0x92407b66d7daf4f30beb84820caae2cbba51add1c4648584101ff3c32151eb83\nEthereum 1 block hash: 0x77b03ebaf0f2835b491cbd99a7f4649a03a6e7999678603030a014a3c48b32a4\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := outputBlockETH1Data(context.Background(), test.eth1Data)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

107
cmd/block/info/process.go Normal file
View File

@@ -0,0 +1,107 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"encoding/json"
"fmt"
"time"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
var jsonOutput bool
var results *dataOut
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
results = &dataOut{
debug: data.debug,
verbose: data.verbose,
eth2Client: data.eth2Client,
}
config, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
}
genesis, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
}
results.genesisTime = genesis.GenesisTime
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon block")
}
if err := outputBlock(ctx, data.jsonOutput, signedBlock); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
if data.stream {
jsonOutput = data.jsonOutput
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
if err != nil {
return nil, errors.Wrap(err, "failed to start block stream")
}
<-ctx.Done()
}
return &dataOut{}, nil
}
func headEventHandler(event *api.Event) {
// Only interested in head events.
if event.Topic != "head" {
return
}
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
if err != nil {
fmt.Printf("Failed to obtain block: %v\n", err)
}
if err := outputBlock(context.Background(), jsonOutput, signedBlock); err != nil {
fmt.Printf("Failed to display block: %v\n", err)
}
}
func outputBlock(ctx context.Context, jsonOutput bool, signedBlock *spec.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
default:
data, err := outputBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
}
return nil
}

View File

@@ -0,0 +1,63 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"os"
"testing"
"github.com/attestantio/go-eth2-client/auto"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
eth2Client, err := auto.New(context.Background(),
auto.WithLogLevel(zerolog.Disabled),
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
)
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Client",
dataIn: &dataIn{
eth2Client: eth2Client,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

50
cmd/block/info/run.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the wallet create data command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

@@ -14,26 +14,13 @@
package cmd
import (
"bytes"
"encoding/hex"
"fmt"
"os"
"sort"
"strings"
"time"
"unicode/utf8"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-bitfield"
"github.com/prysmaticlabs/go-ssz"
"github.com/spf13/cobra"
"github.com/wealdtech/ethdo/grpc"
string2eth "github.com/wealdtech/go-string2eth"
"github.com/spf13/viper"
blockinfo "github.com/wealdtech/ethdo/cmd/block/info"
)
var blockInfoSlot int64
var blockInfoStream bool
var blockInfoCmd = &cobra.Command{
Use: "info",
Short: "Obtain information about a block",
@@ -42,239 +29,37 @@ var blockInfoCmd = &cobra.Command{
ethdo block info --slot=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
err := connect()
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain block")
config, err := grpc.FetchChainConfig(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain configuration")
slotsPerEpoch := config["SlotsPerEpoch"].(uint64)
secondsPerSlot := config["SecondsPerSlot"].(uint64)
genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain genesis")
assert(!blockInfoStream || blockInfoSlot == -1, "--slot and --stream are not supported together")
var slot uint64
if blockInfoSlot < 0 {
slot, err = grpc.FetchLatestFilledSlot(eth2GRPCConn)
errCheck(err, "Failed to obtain slot of latest block")
} else {
slot = uint64(blockInfoSlot)
RunE: func(cmd *cobra.Command, args []string) error {
res, err := blockinfo.Run(cmd)
if err != nil {
return err
}
signedBlock, err := grpc.FetchBlock(eth2GRPCConn, slot)
errCheck(err, "Failed to obtain block")
if signedBlock == nil {
outputIf(!quiet, "No block at that slot")
os.Exit(_exitFailure)
if viper.GetBool("quiet") {
return nil
}
outputBlock(signedBlock, genesisTime, secondsPerSlot, slotsPerEpoch)
if blockInfoStream {
stream, err := grpc.StreamBlocks(eth2GRPCConn)
errCheck(err, "Failed to obtain block stream")
for {
fmt.Println()
signedBlock, err := stream.Recv()
errCheck(err, "Failed to obtain block")
if signedBlock != nil {
outputBlock(signedBlock, genesisTime, secondsPerSlot, slotsPerEpoch)
}
}
if res != "" {
fmt.Println(res)
}
os.Exit(_exitSuccess)
return nil
},
}
func outputBlock(signedBlock *ethpb.SignedBeaconBlock, genesisTime time.Time, secondsPerSlot uint64, slotsPerEpoch uint64) {
block := signedBlock.Block
body := block.Body
// General info.
bodyRoot, err := ssz.HashTreeRoot(block)
errCheck(err, "Failed to calculate block body root")
fmt.Printf("Slot: %d\n", block.Slot)
fmt.Printf("Epoch: %d\n", block.Slot/slotsPerEpoch)
fmt.Printf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(block.Slot*secondsPerSlot), 0))
fmt.Printf("Block root: %#x\n", bodyRoot)
outputIf(verbose, fmt.Sprintf("Parent root: %#x", block.ParentRoot))
outputIf(verbose, fmt.Sprintf("State root: %#x", block.StateRoot))
if len(body.Graffiti) > 0 && hex.EncodeToString(body.Graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
if utf8.Valid(body.Graffiti) {
fmt.Printf("Graffiti: %s\n", string(body.Graffiti))
} else {
fmt.Printf("Graffiti: %#x\n", body.Graffiti)
}
}
// Eth1 data.
eth1Data := body.Eth1Data
outputIf(verbose, fmt.Sprintf("Ethereum 1 deposit count: %d", eth1Data.DepositCount))
outputIf(verbose, fmt.Sprintf("Ethereum 1 deposit root: %#x", eth1Data.DepositRoot))
outputIf(verbose, fmt.Sprintf("Ethereum 1 block hash: %#x", eth1Data.BlockHash))
validatorCommittees := make(map[uint64][][]uint64)
// Attestations.
fmt.Printf("Attestations: %d\n", len(body.Attestations))
if verbose {
for i, att := range body.Attestations {
fmt.Printf("\t%d:\n", i)
// Fetch committees for this epoch if not already obtained.
committees, exists := validatorCommittees[att.Data.Slot]
if !exists {
attestationEpoch := att.Data.Slot / slotsPerEpoch
epochCommittees, err := grpc.FetchValidatorCommittees(eth2GRPCConn, attestationEpoch)
errCheck(err, "Failed to obtain committees")
for k, v := range epochCommittees {
validatorCommittees[k] = v
}
committees = validatorCommittees[att.Data.Slot]
}
fmt.Printf("\t\tCommittee index: %d\n", att.Data.CommitteeIndex)
fmt.Printf("\t\tAttesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len())
fmt.Printf("\t\tAggregation bits: %s\n", bitsToString(att.AggregationBits))
fmt.Printf("\t\tAttesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.CommitteeIndex]))
fmt.Printf("\t\tSlot: %d\n", att.Data.Slot)
fmt.Printf("\t\tBeacon block root: %#x\n", att.Data.BeaconBlockRoot)
fmt.Printf("\t\tSource epoch: %d\n", att.Data.Source.Epoch)
fmt.Printf("\t\tSource root: %#x\n", att.Data.Source.Root)
fmt.Printf("\t\tTarget epoch: %d\n", att.Data.Target.Epoch)
fmt.Printf("\t\tTarget root: %#x\n", att.Data.Target.Root)
}
}
// Attester slashings.
fmt.Printf("Attester slashings: %d\n", len(body.AttesterSlashings))
if verbose {
for i, slashing := range body.AttesterSlashings {
// Say what was slashed.
att1 := slashing.Attestation_1
outputIf(debug, fmt.Sprintf("Attestation 1 attesting indices are %v", att1.AttestingIndices))
att2 := slashing.Attestation_2
outputIf(debug, fmt.Sprintf("Attestation 2 attesting indices are %v", att2.AttestingIndices))
slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices)
if len(slashedIndices) == 0 {
continue
}
fmt.Printf("\t%d:\n", i)
fmt.Println("\t\tSlashed validators:")
for _, slashedIndex := range slashedIndices {
validator, err := grpc.FetchValidatorByIndex(eth2GRPCConn, slashedIndex)
errCheck(err, "Failed to obtain validator information")
fmt.Printf("\t\t\t%#x (%d)\n", validator.PublicKey, slashedIndex)
}
// Say what caused the slashing.
if att1.Data.Target.Epoch == att2.Data.Target.Epoch {
fmt.Printf("\t\tDouble voted for same target epoch (%d):\n", att1.Data.Target.Epoch)
if !bytes.Equal(att1.Data.Target.Root, att2.Data.Target.Root) {
fmt.Printf("\t\t\tAttestation 1 target epoch root: %#x\n", att1.Data.Target.Root)
fmt.Printf("\t\t\tAttestation 2target epoch root: %#x\n", att2.Data.Target.Root)
}
if !bytes.Equal(att1.Data.BeaconBlockRoot, att2.Data.BeaconBlockRoot) {
fmt.Printf("\t\t\tAttestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot)
fmt.Printf("\t\t\tAttestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot)
}
} else if att1.Data.Source.Epoch < att2.Data.Source.Epoch &&
att1.Data.Target.Epoch > att2.Data.Target.Epoch {
fmt.Printf("\t\tSurround voted:\n")
fmt.Printf("\t\t\tAttestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch)
fmt.Printf("\t\t\tAttestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch)
}
}
}
fmt.Printf("Proposer slashings: %d\n", len(body.ProposerSlashings))
// TODO verbose proposer slashings.
// Deposits.
fmt.Printf("Deposits: %d\n", len(body.Deposits))
if verbose {
for i, deposit := range body.Deposits {
data := deposit.Data
fmt.Printf("\t%d:\n", i)
fmt.Printf("\t\tPublic key: %#x\n", data.PublicKey)
fmt.Printf("\t\tAmount: %s\n", string2eth.GWeiToString(data.Amount, true))
fmt.Printf("\t\tWithdrawal credentials: %#x\n", data.WithdrawalCredentials)
fmt.Printf("\t\tSignature: %#x\n", data.Signature)
}
}
// Voluntary exits.
fmt.Printf("Voluntary exits: %d\n", len(body.VoluntaryExits))
if verbose {
for i, voluntaryExit := range body.VoluntaryExits {
fmt.Printf("\t%d:\n", i)
validator, err := grpc.FetchValidatorByIndex(eth2GRPCConn, voluntaryExit.Exit.ValidatorIndex)
errCheck(err, "Failed to obtain validator information")
fmt.Printf("\t\tValidator: %#x (%d)\n", validator.PublicKey, voluntaryExit.Exit.ValidatorIndex)
fmt.Printf("\t\tEpoch: %d\n", voluntaryExit.Exit.Epoch)
}
}
}
// intersection returns a list of items common between the two sets.
func intersection(set1 []uint64, set2 []uint64) []uint64 {
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })
sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] })
res := make([]uint64, 0)
set1Pos := 0
set2Pos := 0
for set1Pos < len(set1) && set2Pos < len(set2) {
switch {
case set1[set1Pos] < set2[set2Pos]:
set1Pos++
case set2[set2Pos] < set1[set1Pos]:
set2Pos++
default:
res = append(res, set1[set1Pos])
set1Pos++
set2Pos++
}
}
return res
}
func bitsToString(input bitfield.Bitlist) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s✓", res)
} else {
res = fmt.Sprintf("%s✕", res)
}
if i%8 == 7 {
res = fmt.Sprintf("%s ", res)
}
}
return strings.TrimSpace(res)
}
func attestingIndices(input bitfield.Bitlist, indices []uint64) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s%d ", res, indices[i])
}
}
return strings.TrimSpace(res)
}
func init() {
blockCmd.AddCommand(blockInfoCmd)
blockFlags(blockInfoCmd)
blockInfoCmd.Flags().Int64Var(&blockInfoSlot, "slot", -1, "the latest slot with a block")
blockInfoCmd.Flags().BoolVar(&blockInfoStream, "stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
}
func blockInfoBindings() {
if err := viper.BindPFlag("blockid", blockInfoCmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockInfoCmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -14,12 +14,16 @@
package cmd
import (
"context"
"fmt"
"os"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/cobra"
"github.com/wealdtech/ethdo/grpc"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
)
var chainInfoCmd = &cobra.Command{
@@ -31,31 +35,31 @@ var chainInfoCmd = &cobra.Command{
In quiet mode this will return 0 if the chain information can be obtained, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
err := connect()
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node")
config, err := grpc.FetchChainConfig(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain configuration")
ctx := context.Background()
genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn)
errCheck(err, "Failed to obtain genesis time")
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")
genesisValidatorsRoot, err := grpc.FetchGenesisValidatorsRoot(eth2GRPCConn)
errCheck(err, "Failed to obtain genesis validators root")
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
errCheck(err, "Failed to obtain beacon chain specification")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
errCheck(err, "Failed to obtain beacon chain genesis")
if quiet {
os.Exit(_exitSuccess)
}
if genesisTime.Unix() == 0 {
if genesis.GenesisTime.Unix() == 0 {
fmt.Println("Genesis time: undefined")
} else {
fmt.Printf("Genesis time: %s\n", genesisTime.Format(time.UnixDate))
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesisTime.Unix()))
fmt.Printf("Genesis time: %s\n", genesis.GenesisTime.Format(time.UnixDate))
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix()))
}
fmt.Printf("Genesis validators root: %#x\n", genesisValidatorsRoot)
fmt.Printf("Genesis fork version: %#x\n", config["GenesisForkVersion"].([]byte))
fmt.Printf("Seconds per slot: %d\n", config["SecondsPerSlot"].(uint64))
fmt.Printf("Slots per epoch: %d\n", config["SlotsPerEpoch"].(uint64))
fmt.Printf("Genesis validators root: %#x\n", genesis.GenesisValidatorsRoot)
fmt.Printf("Genesis fork version: %#x\n", config["GENESIS_FORK_VERSION"].([]byte))
fmt.Printf("Seconds per slot: %d\n", int(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
fmt.Printf("Slots per epoch: %d\n", config["SLOTS_PER_EPOCH"].(uint64))
os.Exit(_exitSuccess)
},
@@ -66,17 +70,17 @@ func init() {
chainFlags(chainInfoCmd)
}
func timestampToSlot(genesis int64, timestamp int64, secondsPerSlot uint64) uint64 {
if timestamp < genesis {
func timestampToSlot(genesis time.Time, timestamp time.Time, secondsPerSlot time.Duration) spec.Slot {
if timestamp.Unix() < genesis.Unix() {
return 0
}
return uint64(timestamp-genesis) / secondsPerSlot
return spec.Slot(uint64(timestamp.Unix()-genesis.Unix()) / uint64(secondsPerSlot.Seconds()))
}
func slotToTimestamp(genesis int64, slot uint64, secondsPerSlot uint64) int64 {
return genesis + int64(slot*secondsPerSlot)
func slotToTimestamp(genesis time.Time, slot spec.Slot, slotDuration time.Duration) int64 {
return genesis.Unix() + int64(slot)*int64(slotDuration.Seconds())
}
func epochToTimestamp(genesis int64, slot uint64, secondsPerSlot uint64, slotsPerEpoch uint64) int64 {
return genesis + int64(slot*secondsPerSlot*slotsPerEpoch)
func epochToTimestamp(genesis time.Time, slot spec.Slot, slotDuration time.Duration, slotsPerEpoch uint64) int64 {
return genesis.Unix() + int64(slot)*int64(slotDuration.Seconds())*int64(slotsPerEpoch)
}

View File

@@ -14,16 +14,18 @@
package cmd
import (
"context"
"fmt"
"os"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/cobra"
"github.com/wealdtech/ethdo/grpc"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
)
var chainStatusSlot bool
var chainStatusCmd = &cobra.Command{
Use: "status",
Short: "Obtain status about a chain",
@@ -33,70 +35,48 @@ var chainStatusCmd = &cobra.Command{
In quiet mode this will return 0 if the chain status can be obtained, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
err := connect()
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node")
config, err := grpc.FetchChainConfig(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain configuration")
ctx := context.Background()
genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn)
errCheck(err, "Failed to obtain genesis time")
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")
info, err := grpc.FetchChainInfo(eth2GRPCConn)
errCheck(err, "Failed to obtain chain info")
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
errCheck(err, "Failed to obtain beacon chain specification")
if quiet {
os.Exit(_exitSuccess)
finality, err := eth2Client.(eth2client.FinalityProvider).Finality(ctx, "head")
errCheck(err, "Failed to obtain finality information")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
errCheck(err, "Failed to obtain genesis information")
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
curSlot := timestampToSlot(genesis.GenesisTime, time.Now(), slotDuration)
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
curEpoch := spec.Epoch(uint64(curSlot) / slotsPerEpoch)
fmt.Printf("Current epoch: %d\n", curEpoch)
fmt.Printf("Justified epoch: %d\n", finality.Justified.Epoch)
if verbose {
distance := curEpoch - finality.Justified.Epoch
fmt.Printf("Justified epoch distance: %d\n", distance)
}
now := time.Now()
slot := timestampToSlot(genesisTime.Unix(), now.Unix(), config["SecondsPerSlot"].(uint64))
if chainStatusSlot {
fmt.Printf("Current slot: %d\n", slot)
fmt.Printf("Justified slot: %d\n", info.GetJustifiedSlot())
if verbose {
distance := slot - info.GetJustifiedSlot()
fmt.Printf("Justified slot distance: %d\n", distance)
}
fmt.Printf("Finalized slot: %d\n", info.GetFinalizedSlot())
if verbose {
distance := slot - info.GetFinalizedSlot()
fmt.Printf("Finalized slot distance: %d\n", distance)
}
if verbose {
fmt.Printf("Prior justified slot: %d\n", info.GetFinalizedSlot())
distance := slot - info.GetPreviousJustifiedSlot()
fmt.Printf("Prior justified slot distance: %d\n", distance)
}
} else {
slotsPerEpoch := config["SlotsPerEpoch"].(uint64)
epoch := slot / slotsPerEpoch
fmt.Printf("Current epoch: %d\n", epoch)
fmt.Printf("Justified epoch: %d\n", info.GetJustifiedEpoch())
if verbose {
distance := (slot - info.GetJustifiedSlot()) / slotsPerEpoch
fmt.Printf("Justified epoch distance: %d\n", distance)
}
fmt.Printf("Finalized epoch: %d\n", info.GetFinalizedEpoch())
if verbose {
distance := (slot - info.GetFinalizedSlot()) / slotsPerEpoch
fmt.Printf("Finalized epoch distance: %d\n", distance)
}
if verbose {
fmt.Printf("Prior justified epoch: %d\n", info.GetPreviousJustifiedEpoch())
distance := (slot - info.GetPreviousJustifiedSlot()) / slotsPerEpoch
fmt.Printf("Prior justified epoch distance: %d\n", distance)
}
fmt.Printf("Finalized epoch: %d\n", finality.Finalized.Epoch)
if verbose {
distance := curEpoch - finality.Finalized.Epoch
fmt.Printf("Finalized epoch distance: %d\n", distance)
}
if verbose {
fmt.Printf("Prior justified epoch: %d\n", finality.PreviousJustified.Epoch)
distance := curEpoch - finality.PreviousJustified.Epoch
fmt.Printf("Prior justified epoch distance: %d\n", distance)
}
if verbose {
slotsPerEpoch := config["SlotsPerEpoch"].(uint64)
secondsPerSlot := config["SecondsPerSlot"].(uint64)
epochStartSlot := (slot / slotsPerEpoch) * slotsPerEpoch
epochStartSlot := (uint64(curSlot) / slotsPerEpoch) * slotsPerEpoch
fmt.Printf("Epoch slots: %d-%d\n", epochStartSlot, epochStartSlot+slotsPerEpoch-1)
nextSlot := slotToTimestamp(genesisTime.Unix(), slot+1, secondsPerSlot)
fmt.Printf("Time until next slot: %2.1fs\n", float64(time.Until(time.Unix(nextSlot, 0)).Milliseconds())/1000)
nextEpoch := epochToTimestamp(genesisTime.Unix(), slot/slotsPerEpoch+1, secondsPerSlot, slotsPerEpoch)
fmt.Printf("Slots until next epoch: %d\n", (slot/slotsPerEpoch+1)*slotsPerEpoch-slot)
nextSlotTimestamp := slotToTimestamp(genesis.GenesisTime, curSlot+1, slotDuration)
fmt.Printf("Time until next slot: %2.1fs\n", float64(time.Until(time.Unix(nextSlotTimestamp, 0)).Milliseconds())/1000)
nextEpoch := epochToTimestamp(genesis.GenesisTime, spec.Slot(uint64(curSlot)/slotsPerEpoch+1), slotDuration, slotsPerEpoch)
fmt.Printf("Slots until next epoch: %d\n", (uint64(curSlot)/slotsPerEpoch+1)*slotsPerEpoch-uint64(curSlot))
fmt.Printf("Time until next epoch: %2.1fs\n", float64(time.Until(time.Unix(nextEpoch, 0)).Milliseconds())/1000)
}
@@ -107,6 +87,4 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
func init() {
chainCmd.AddCommand(chainStatusCmd)
chainFlags(chainStatusCmd)
chainStatusCmd.Flags().BoolVar(&chainStatusSlot, "slot", false, "Print slot-based values")
}

View File

@@ -14,6 +14,7 @@
package cmd
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
@@ -22,11 +23,11 @@ import (
"os"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/grpc"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
@@ -43,8 +44,7 @@ var exitVerifyCmd = &cobra.Command{
In quiet mode this will return 0 if the the exit is verified correctly, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
ctx := context.Background()
assert(viper.GetString("account") != "" || exitVerifyPubKey != "", "account or public key is required")
account, err := exitVerifyAccount(ctx)
@@ -55,23 +55,26 @@ In quiet mode this will return 0 if the the exit is verified correctly, otherwis
errCheck(err, "Failed to obtain exit data")
// Confirm signature is good.
err = connect()
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node")
genesisValidatorsRoot, err := grpc.FetchGenesisValidatorsRoot(eth2GRPCConn)
outputIf(debug, fmt.Sprintf("Genesis validators root is %#x", genesisValidatorsRoot))
errCheck(err, "Failed to obtain genesis validators root")
domain := e2types.Domain(e2types.DomainVoluntaryExit, data.ForkVersion, genesisValidatorsRoot)
exit := &ethpb.VoluntaryExit{
Epoch: data.Epoch,
ValidatorIndex: data.ValidatorIndex,
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")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
errCheck(err, "Failed to obtain beacon chain genesis")
domain := e2types.Domain(e2types.DomainVoluntaryExit, data.ForkVersion[:], genesis.GenesisValidatorsRoot[:])
exit := &spec.VoluntaryExit{
Epoch: data.Data.Message.Epoch,
ValidatorIndex: data.Data.Message.ValidatorIndex,
}
sig, err := e2types.BLSSignatureFromBytes(data.Signature)
sig, err := e2types.BLSSignatureFromBytes(data.Data.Signature[:])
errCheck(err, "Invalid signature")
verified, err := verifyStruct(account, exit, domain, sig)
errCheck(err, "Failed to verify voluntary exit")
assert(verified, "Voluntary exit failed to verify")
// TODO confirm fork version is valid (once we have a way of obtaining the current fork version).
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain current fork")
assert(bytes.Equal(data.ForkVersion[:], fork.CurrentVersion[:]) || bytes.Equal(data.ForkVersion[:], fork.PreviousVersion[:]), "Exit is for an old fork version and is no longer valid")
outputIf(verbose, "Verified")
os.Exit(_exitSuccess)
@@ -79,7 +82,7 @@ In quiet mode this will return 0 if the the exit is verified correctly, otherwis
}
// obtainExitData obtains exit data from an input, could be JSON itself or a path to JSON.
func obtainExitData(input string) (*validatorExitData, error) {
func obtainExitData(input string) (*util.ValidatorExitData, error) {
var err error
var data []byte
// Input could be JSON or a path to JSON
@@ -93,7 +96,7 @@ func obtainExitData(input string) (*validatorExitData, error) {
return nil, errors.Wrap(err, "failed to find deposit data file")
}
}
exitData := &validatorExitData{}
exitData := &util.ValidatorExitData{}
err = json.Unmarshal(data, exitData)
if err != nil {
return nil, errors.Wrap(err, "data is not valid JSON")

View File

@@ -1,46 +0,0 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/wealdtech/ethdo/grpc"
)
// networks is a map of deposit contract addresses to networks.
var networks = map[string]string{
"16e82d77882a663454ef92806b7deca1d394810f": "Altona",
"0f0f0fc0530007361933eab5db97d09acdd6c1c8": "Onyx",
"07b39f4fde4a38bace212b546dac87c58dfe3fdc": "Medalla",
}
// network returns the name of the network, if known.
func network() string {
if err := connect(); err != nil {
return "Unknown"
}
depositContractAddress, err := grpc.FetchDepositContractAddress(eth2GRPCConn)
if err != nil {
return "Unknown"
}
outputIf(debug, fmt.Sprintf("Deposit contract is %x", depositContractAddress))
depositContract := fmt.Sprintf("%x", depositContractAddress)
if network, exists := networks[depositContract]; exists {
return network
}
return "Unknown"
}

View File

@@ -14,12 +14,14 @@
package cmd
import (
"context"
"fmt"
"os"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/spf13/cobra"
"github.com/wealdtech/ethdo/grpc"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
)
var nodeInfoCmd = &cobra.Command{
@@ -31,38 +33,24 @@ var nodeInfoCmd = &cobra.Command{
In quiet mode this will return 0 if the node information can be obtained, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
err := connect()
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node")
config, err := grpc.FetchChainConfig(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain configuration")
ctx := context.Background()
genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn)
errCheck(err, "Failed to obtain genesis time")
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")
if quiet {
os.Exit(_exitSuccess)
}
if verbose {
version, metadata, err := grpc.FetchVersion(eth2GRPCConn)
errCheck(err, "Failed to obtain version")
version, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx)
errCheck(err, "Failed to obtain node version")
fmt.Printf("Version: %s\n", version)
if metadata != "" {
fmt.Printf("Metadata: %s\n", metadata)
}
}
syncing, err := grpc.FetchSyncing(eth2GRPCConn)
errCheck(err, "Failed to obtain syncing state")
fmt.Printf("Syncing: %v\n", syncing)
if genesisTime.Unix() == 0 {
fmt.Println("Not reached genesis")
} else {
slot := timestampToSlot(genesisTime.Unix(), time.Now().Unix(), config["SecondsPerSlot"].(uint64))
fmt.Printf("Current slot: %d\n", slot)
fmt.Printf("Current epoch: %d\n", slot/config["SlotsPerEpoch"].(uint64))
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesisTime.Unix()))
}
syncState, err := eth2Client.(eth2client.NodeSyncingProvider).NodeSyncing(ctx)
errCheck(err, "failed to obtain node sync state")
fmt.Printf("Syncing: %t\n", syncState.SyncDistance != 0)
os.Exit(_exitSuccess)
},

View File

@@ -31,7 +31,6 @@ import (
filesystem "github.com/wealdtech/go-eth2-wallet-store-filesystem"
s3 "github.com/wealdtech/go-eth2-wallet-store-s3"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
"google.golang.org/grpc"
)
var cfgFile string
@@ -45,9 +44,6 @@ var rootStore string
// Store for wallet actions.
var store e2wtypes.Store
// Prysm connection.
var eth2GRPCConn *grpc.ClientConn
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "ethdo",
@@ -80,10 +76,16 @@ func persistentPreRun(cmd *cobra.Command, args []string) {
accountImportBindings()
case "attester/inclusion":
attesterInclusionBindings()
case "block/info":
blockInfoBindings()
case "exit/verify":
exitVerifyBindings()
case "validator/depositdata":
validatorDepositdataBindings()
case "validator/exit":
validatorExitBindings()
case "validator/info":
validatorInfoBindings()
case "wallet/create":
walletCreateBindings()
case "wallet/import":
@@ -182,7 +184,7 @@ func init() {
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("connection", "localhost:4000", "connection to Ethereum 2 node via GRPC")
RootCmd.PersistentFlags().String("connection", "localhost:4000", "connection to an Ethereum 2 node")
if err := viper.BindPFlag("connection", RootCmd.PersistentFlags().Lookup("connection")); err != nil {
panic(err)
}
@@ -210,6 +212,10 @@ func init() {
if err := viper.BindPFlag("allow-weak-passphrases", RootCmd.PersistentFlags().Lookup("allow-weak-passphrases")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Bool("allow-insecure-connections", false, "allow insecure connections to remote beacon nodes")
if err := viper.BindPFlag("allow-insecure-connections", RootCmd.PersistentFlags().Lookup("allow-insecure-connections")); err != nil {
panic(err)
}
}
// initConfig reads in config file and ENV variables if set.
@@ -331,32 +337,6 @@ func walletAndAccountFromPath(ctx context.Context, path string) (e2wtypes.Wallet
return wallet, account, nil
}
// connect connects to an Ethereum 2 endpoint.
func connect() error {
if eth2GRPCConn != nil {
// Already connected.
return nil
}
connection := ""
if viper.GetString("connection") != "" {
connection = viper.GetString("connection")
}
if connection == "" {
return errors.New("no connection")
}
outputIf(debug, fmt.Sprintf("Connecting to %s", connection))
opts := []grpc.DialOption{grpc.WithInsecure()}
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
var err error
eth2GRPCConn, err = grpc.DialContext(ctx, connection, opts...)
return err
}
// bestPublicKey returns the best public key for operations.
// It prefers the composite public key if present, otherwise the public key.
func bestPublicKey(account e2wtypes.Account) (e2types.PublicKey, error) {

View File

@@ -24,17 +24,6 @@ import (
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// signStruct signs an arbitrary structure.
func signStruct(account e2wtypes.Account, data interface{}, domain []byte) (e2types.Signature, error) {
objRoot, err := ssz.HashTreeRoot(data)
outputIf(debug, fmt.Sprintf("Object root is %#x", objRoot))
if err != nil {
return nil, err
}
return signRoot(account, objRoot, domain)
}
// verifyStruct verifies the signature of an arbitrary structure.
func verifyStruct(account e2wtypes.Account, data interface{}, domain []byte, signature e2types.Signature) (bool, error) {
objRoot, err := ssz.HashTreeRoot(data)

View File

@@ -18,10 +18,12 @@ import (
"encoding/hex"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/core"
"github.com/wealdtech/ethdo/grpc"
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,15 +33,19 @@ import (
type dataIn struct {
format string
withdrawalCredentials []byte
amount uint64
amount spec.Gwei
validatorAccounts []e2wtypes.Account
forkVersion []byte
domain []byte
forkVersion *spec.Version
domain *spec.Domain
passphrases []string
}
func input() (*dataIn, error) {
var err error
data := &dataIn{}
data := &dataIn{
forkVersion: &spec.Version{},
domain: &spec.Domain{},
}
if viper.GetString("validatoraccount") == "" {
return nil, errors.New("validator account is required")
@@ -64,6 +70,8 @@ func input() (*dataIn, error) {
data.format = "json"
}
data.passphrases = ethdoutil.GetPassphrases()
switch {
case viper.GetString("withdrawalaccount") != "":
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
@@ -99,39 +107,49 @@ func input() (*dataIn, error) {
if viper.GetString("depositvalue") == "" {
return nil, errors.New("deposit value is required")
}
data.amount, err = string2eth.StringToGWei(viper.GetString("depositvalue"))
amount, err := string2eth.StringToGWei(viper.GetString("depositvalue"))
if err != nil {
return nil, errors.Wrap(err, "deposit value is invalid")
}
data.amount = spec.Gwei(amount)
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
if data.amount < 1000000000 { // MIN_DEPOSIT_AMOUNT
return nil, errors.New("deposit value must be at least 1 Ether")
}
if viper.GetString("forkversion") != "" {
data.forkVersion, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("forkversion"), "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode fork version")
}
if len(data.forkVersion) != 4 {
return nil, errors.New("fork version must be exactly 4 bytes in length")
}
} else {
conn, err := grpc.Connect()
if err != nil {
return nil, errors.Wrap(err, "failed to connect to beacon node")
}
config, err := grpc.FetchChainConfig(conn)
if err != nil {
return nil, errors.Wrap(err, "could not connect to beacon node; supply a connection with --connection or provide a fork version with --forkversion to generate deposit data")
}
genesisForkVersion, exists := config["GenesisForkVersion"]
if !exists {
return nil, errors.New("failed to obtain genesis fork version")
}
data.forkVersion = genesisForkVersion.([]byte)
data.forkVersion, err = inputForkVersion(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain fork version")
}
data.domain = e2types.Domain(e2types.DomainDeposit, data.forkVersion, e2types.ZeroGenesisValidatorsRoot)
copy(data.domain[:], e2types.Domain(e2types.DomainDeposit, data.forkVersion[:], e2types.ZeroGenesisValidatorsRoot))
return data, nil
}
func inputForkVersion(ctx context.Context) (*spec.Version, error) {
if viper.GetString("forkversion") != "" {
forkVersion, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("forkversion"), "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode fork version")
}
if len(forkVersion) != 4 {
return nil, errors.New("fork version must be exactly 4 bytes in length")
}
res := &spec.Version{}
copy(res[:], forkVersion)
return res, nil
}
eth2Client, err := ethdoutil.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis")
}
return &genesis.GenesisForkVersion, nil
}

View File

@@ -17,8 +17,10 @@ import (
"context"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
@@ -39,17 +41,28 @@ func TestInput(t *testing.T) {
viper.Set("passphrase", "pass")
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 1",
hexToBytes("0x51d0b65185db6989ab0b560d6deed19c7ead0e24b9b6372cbecb1f26bdfad000"),
testutil.HexToBytes("0x51d0b65185db6989ab0b560d6deed19c7ead0e24b9b6372cbecb1f26bdfad000"),
[]byte("pass"),
)
require.NoError(t, err)
var forkVersion *spec.Version
{
tmp := testutil.HexToVersion("0x01020304")
forkVersion = &tmp
}
var domain *spec.Domain
{
tmp := testutil.HexToDomain("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0")
domain = &tmp
}
tests := []struct {
name string
vars map[string]interface{}
@@ -167,17 +180,7 @@ func TestInput(t *testing.T) {
"depositvalue": "32 Ether",
"forkversion": "invalid",
},
err: "failed to decode fork version: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "ForkVersionWrongLength",
vars: map[string]interface{}{
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x0102030405",
},
err: "fork version must be exactly 4 bytes in length",
err: "failed to obtain fork version: failed to decode fork version: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "Good",
@@ -189,11 +192,11 @@ func TestInput(t *testing.T) {
},
res: &dataIn{
format: "json",
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: hexToBytes("0x01020304"),
domain: hexToBytes("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0"),
forkVersion: forkVersion,
domain: domain,
},
},
{
@@ -206,11 +209,11 @@ func TestInput(t *testing.T) {
},
res: &dataIn{
format: "json",
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: hexToBytes("0x01020304"),
domain: hexToBytes("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0"),
forkVersion: forkVersion,
domain: domain,
},
},
}

View File

@@ -17,19 +17,20 @@ import (
"fmt"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type dataOut struct {
format string
account string
validatorPubKey []byte
validatorPubKey *spec.BLSPubKey
withdrawalCredentials []byte
amount uint64
signature []byte
forkVersion []byte
depositDataRoot []byte
depositMessageRoot []byte
amount spec.Gwei
signature *spec.BLSSignature
forkVersion *spec.Version
depositDataRoot *spec.Root
depositMessageRoot *spec.Root
}
func output(data []*dataOut) (string, error) {
@@ -57,8 +58,8 @@ func output(data []*dataOut) (string, error) {
}
func validatorDepositDataOutputRaw(datum *dataOut) (string, error) {
if len(datum.validatorPubKey) != 48 {
return "", errors.New("validator public key must be 48 bytes")
if datum.validatorPubKey == nil {
return "", errors.New("validator public key required")
}
if len(datum.withdrawalCredentials) != 32 {
return "", errors.New("withdrawal credentials must be 32 bytes")
@@ -66,14 +67,11 @@ func validatorDepositDataOutputRaw(datum *dataOut) (string, error) {
if datum.amount == 0 {
return "", errors.New("missing amount")
}
if len(datum.signature) != 96 {
return "", errors.New("signature must be 96 bytes")
if datum.signature == nil {
return "", errors.New("signature required")
}
if len(datum.depositMessageRoot) != 32 {
return "", errors.New("deposit message root must be 32 bytes")
}
if len(datum.depositDataRoot) != 32 {
return "", errors.New("deposit data root must be 32 bytes")
if datum.depositDataRoot == nil {
return "", errors.New("deposit data root required")
}
output := fmt.Sprintf(
@@ -98,17 +96,17 @@ func validatorDepositDataOutputRaw(datum *dataOut) (string, error) {
"0000000000000000000000000000000000000000000000000000000000000060"+
"%x"+
`"`,
datum.depositDataRoot,
datum.validatorPubKey,
*datum.depositDataRoot,
*datum.validatorPubKey,
datum.withdrawalCredentials,
datum.signature,
*datum.signature,
)
return output, nil
}
func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) {
if len(datum.validatorPubKey) != 48 {
return "", errors.New("validator public key must be 48 bytes")
if datum.validatorPubKey == nil {
return "", errors.New("validator public key required")
}
if len(datum.withdrawalCredentials) != 32 {
return "", errors.New("withdrawal credentials must be 32 bytes")
@@ -116,24 +114,24 @@ func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) {
if datum.amount == 0 {
return "", errors.New("missing amount")
}
if len(datum.signature) != 96 {
return "", errors.New("signature must be 96 bytes")
if datum.signature == nil {
return "", errors.New("signature required")
}
if len(datum.depositMessageRoot) != 32 {
return "", errors.New("deposit message root must be 32 bytes")
if datum.depositMessageRoot == nil {
return "", errors.New("deposit message root required")
}
if len(datum.depositDataRoot) != 32 {
return "", errors.New("deposit data root must be 32 bytes")
if datum.depositDataRoot == nil {
return "", errors.New("deposit data root required")
}
output := fmt.Sprintf(`{"pubkey":"%x","withdrawal_credentials":"%x","amount":%d,"signature":"%x","deposit_message_root":"%x","deposit_data_root":"%x","fork_version":"%x"}`,
datum.validatorPubKey,
*datum.validatorPubKey,
datum.withdrawalCredentials,
datum.amount,
datum.signature,
datum.depositMessageRoot,
datum.depositDataRoot,
datum.forkVersion,
*datum.signature,
*datum.depositMessageRoot,
*datum.depositDataRoot,
*datum.forkVersion,
)
return output, nil
}
@@ -142,30 +140,30 @@ func validatorDepositDataOutputJSON(datum *dataOut) (string, error) {
if datum.account == "" {
return "", errors.New("missing account")
}
if len(datum.validatorPubKey) != 48 {
return "", errors.New("validator public key must be 48 bytes")
if datum.validatorPubKey == nil {
return "", errors.New("validator public key required")
}
if len(datum.withdrawalCredentials) != 32 {
return "", errors.New("withdrawal credentials must be 32 bytes")
}
if len(datum.signature) != 96 {
return "", errors.New("signature must be 96 bytes")
if datum.signature == nil {
return "", errors.New("signature required")
}
if datum.amount == 0 {
return "", errors.New("missing amount")
}
if len(datum.depositDataRoot) != 32 {
return "", errors.New("deposit data root must be 32 bytes")
if datum.depositDataRoot == nil {
return "", errors.New("deposit data root required")
}
output := fmt.Sprintf(`{"name":"Deposit for %s","account":"%s","pubkey":"%#x","withdrawal_credentials":"%#x","signature":"%#x","value":%d,"deposit_data_root":"%#x","version":2}`,
datum.account,
datum.account,
datum.validatorPubKey,
*datum.validatorPubKey,
datum.withdrawalCredentials,
datum.signature,
*datum.signature,
datum.amount,
datum.depositDataRoot,
*datum.depositDataRoot,
)
return output, nil
}

View File

@@ -14,22 +14,61 @@
package depositdata
import (
"encoding/hex"
"strings"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
)
func hexToBytes(input string) []byte {
res, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
if err != nil {
panic(err)
}
return res
}
func TestOutputJSON(t *testing.T) {
var validatorPubKey *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c")
validatorPubKey = &tmp
}
var signature *spec.BLSSignature
{
tmp := testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2")
signature = &tmp
}
var forkVersion *spec.Version
{
tmp := testutil.HexToVersion("0x01020304")
forkVersion = &tmp
}
var depositDataRoot *spec.Root
{
tmp := testutil.HexToRoot("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554")
depositDataRoot = &tmp
}
var depositMessageRoot *spec.Root
{
tmp := testutil.HexToRoot("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6")
depositMessageRoot = &tmp
}
var validatorPubKey2 *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b")
validatorPubKey2 = &tmp
}
var signature2 *spec.BLSSignature
{
tmp := testutil.HexToSignature("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e")
signature2 = &tmp
}
var depositDataRoot2 *spec.Root
{
tmp := testutil.HexToRoot("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab")
depositDataRoot2 = &tmp
}
var depositMessageRoot2 *spec.Root
{
tmp := testutil.HexToRoot("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52")
depositMessageRoot2 = &tmp
}
tests := []struct {
name string
dataOut []*dataOut
@@ -52,13 +91,13 @@ func TestOutputJSON(t *testing.T) {
dataOut: []*dataOut{
{
format: "json",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "missing account",
@@ -69,15 +108,15 @@ func TestOutputJSON(t *testing.T) {
{
format: "json",
account: "interop/00000",
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "validator public key must be 48 bytes",
err: "validator public key required",
},
{
name: "MissingWithdrawalCredentials",
@@ -85,12 +124,12 @@ func TestOutputJSON(t *testing.T) {
{
format: "json",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
validatorPubKey: validatorPubKey,
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "withdrawal credentials must be 32 bytes",
@@ -101,15 +140,15 @@ func TestOutputJSON(t *testing.T) {
{
format: "json",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "signature must be 96 bytes",
err: "signature required",
},
{
name: "AmountMissing",
@@ -117,12 +156,12 @@ func TestOutputJSON(t *testing.T) {
{
format: "json",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "missing amount",
@@ -133,15 +172,15 @@ func TestOutputJSON(t *testing.T) {
{
format: "json",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositMessageRoot: depositMessageRoot,
},
},
err: "deposit data root must be 32 bytes",
err: "deposit data root required",
},
{
name: "Single",
@@ -149,13 +188,13 @@ func TestOutputJSON(t *testing.T) {
{
format: "json",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
res: `[{"name":"Deposit for interop/00000","account":"interop/00000","pubkey":"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","signature":"0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","value":32000000000,"deposit_data_root":"0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","version":2}]`,
@@ -166,24 +205,24 @@ func TestOutputJSON(t *testing.T) {
{
format: "json",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
{
format: "json",
account: "interop/00001",
validatorPubKey: hexToBytes("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b"),
withdrawalCredentials: hexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"),
validatorPubKey: validatorPubKey2,
withdrawalCredentials: testutil.HexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"),
amount: 32000000000,
signature: hexToBytes("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab"),
depositMessageRoot: hexToBytes("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52"),
signature: signature2,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot2,
depositMessageRoot: depositMessageRoot2,
},
},
res: `[{"name":"Deposit for interop/00000","account":"interop/00000","pubkey":"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","signature":"0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","value":32000000000,"deposit_data_root":"0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","version":2},{"name":"Deposit for interop/00001","account":"interop/00001","pubkey":"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","withdrawal_credentials":"0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594","signature":"0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e","value":32000000000,"deposit_data_root":"0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab","version":2}]`,
@@ -204,6 +243,53 @@ func TestOutputJSON(t *testing.T) {
}
func TestOutputLaunchpad(t *testing.T) {
var validatorPubKey *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c")
validatorPubKey = &tmp
}
var signature *spec.BLSSignature
{
tmp := testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2")
signature = &tmp
}
var forkVersion *spec.Version
{
tmp := testutil.HexToVersion("0x01020304")
forkVersion = &tmp
}
var depositDataRoot *spec.Root
{
tmp := testutil.HexToRoot("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554")
depositDataRoot = &tmp
}
var depositMessageRoot *spec.Root
{
tmp := testutil.HexToRoot("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6")
depositMessageRoot = &tmp
}
var validatorPubKey2 *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b")
validatorPubKey2 = &tmp
}
var signature2 *spec.BLSSignature
{
tmp := testutil.HexToSignature("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e")
signature2 = &tmp
}
var depositDataRoot2 *spec.Root
{
tmp := testutil.HexToRoot("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab")
depositDataRoot2 = &tmp
}
var depositMessageRoot2 *spec.Root
{
tmp := testutil.HexToRoot("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52")
depositMessageRoot2 = &tmp
}
tests := []struct {
name string
dataOut []*dataOut
@@ -227,15 +313,15 @@ func TestOutputLaunchpad(t *testing.T) {
{
format: "launchpad",
account: "interop/00000",
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "validator public key must be 48 bytes",
err: "validator public key required",
},
{
name: "MissingWithdrawalCredentials",
@@ -243,12 +329,12 @@ func TestOutputLaunchpad(t *testing.T) {
{
format: "launchpad",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
validatorPubKey: validatorPubKey,
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "withdrawal credentials must be 32 bytes",
@@ -259,15 +345,15 @@ func TestOutputLaunchpad(t *testing.T) {
{
format: "launchpad",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "signature must be 96 bytes",
err: "signature required",
},
{
name: "AmountMissing",
@@ -275,12 +361,12 @@ func TestOutputLaunchpad(t *testing.T) {
{
format: "launchpad",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "missing amount",
@@ -291,15 +377,15 @@ func TestOutputLaunchpad(t *testing.T) {
{
format: "launchpad",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositMessageRoot: depositMessageRoot,
},
},
err: "deposit data root must be 32 bytes",
err: "deposit data root required",
},
{
name: "DepositMessageRootMissing",
@@ -307,15 +393,15 @@ func TestOutputLaunchpad(t *testing.T) {
{
format: "launchpad",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
},
},
err: "deposit message root must be 32 bytes",
err: "deposit message root required",
},
{
name: "Single",
@@ -323,13 +409,13 @@ func TestOutputLaunchpad(t *testing.T) {
{
format: "launchpad",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"01020304"}]`,
@@ -340,24 +426,24 @@ func TestOutputLaunchpad(t *testing.T) {
{
format: "launchpad",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
{
format: "launchpad",
account: "interop/00001",
validatorPubKey: hexToBytes("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b"),
withdrawalCredentials: hexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"),
validatorPubKey: validatorPubKey2,
withdrawalCredentials: testutil.HexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"),
amount: 32000000000,
signature: hexToBytes("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab"),
depositMessageRoot: hexToBytes("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52"),
signature: signature2,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot2,
depositMessageRoot: depositMessageRoot2,
},
},
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"01020304"},{"pubkey":"b89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","withdrawal_credentials":"00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594","amount":32000000000,"signature":"911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e","deposit_message_root":"bb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52","deposit_data_root":"3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab","fork_version":"01020304"}]`,
@@ -378,6 +464,53 @@ func TestOutputLaunchpad(t *testing.T) {
}
func TestOutputRaw(t *testing.T) {
var validatorPubKey *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c")
validatorPubKey = &tmp
}
var signature *spec.BLSSignature
{
tmp := testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2")
signature = &tmp
}
var forkVersion *spec.Version
{
tmp := testutil.HexToVersion("0x01020304")
forkVersion = &tmp
}
var depositDataRoot *spec.Root
{
tmp := testutil.HexToRoot("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554")
depositDataRoot = &tmp
}
var depositMessageRoot *spec.Root
{
tmp := testutil.HexToRoot("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6")
depositMessageRoot = &tmp
}
var validatorPubKey2 *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b")
validatorPubKey2 = &tmp
}
var signature2 *spec.BLSSignature
{
tmp := testutil.HexToSignature("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e")
signature2 = &tmp
}
var depositDataRoot2 *spec.Root
{
tmp := testutil.HexToRoot("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab")
depositDataRoot2 = &tmp
}
var depositMessageRoot2 *spec.Root
{
tmp := testutil.HexToRoot("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52")
depositMessageRoot2 = &tmp
}
tests := []struct {
name string
dataOut []*dataOut
@@ -401,15 +534,15 @@ func TestOutputRaw(t *testing.T) {
{
format: "raw",
account: "interop/00000",
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "validator public key must be 48 bytes",
err: "validator public key required",
},
{
name: "MissingWithdrawalCredentials",
@@ -417,12 +550,12 @@ func TestOutputRaw(t *testing.T) {
{
format: "raw",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
validatorPubKey: validatorPubKey,
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "withdrawal credentials must be 32 bytes",
@@ -433,15 +566,15 @@ func TestOutputRaw(t *testing.T) {
{
format: "raw",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "signature must be 96 bytes",
err: "signature required",
},
{
name: "AmountMissing",
@@ -449,12 +582,12 @@ func TestOutputRaw(t *testing.T) {
{
format: "raw",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
err: "missing amount",
@@ -465,31 +598,15 @@ func TestOutputRaw(t *testing.T) {
{
format: "raw",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositMessageRoot: depositMessageRoot,
},
},
err: "deposit data root must be 32 bytes",
},
{
name: "DepositMessageRootMissing",
dataOut: []*dataOut{
{
format: "raw",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
},
},
err: "deposit message root must be 32 bytes",
err: "deposit data root required",
},
{
name: "Single",
@@ -497,13 +614,13 @@ func TestOutputRaw(t *testing.T) {
{
format: "raw",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
res: `["0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001209e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a35540000000000000000000000000000000000000000000000000000000000000030a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b0000000000000000000000000000000000000000000000000000000000000060b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"]`,
@@ -514,24 +631,24 @@ func TestOutputRaw(t *testing.T) {
{
format: "raw",
account: "interop/00000",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
{
format: "raw",
account: "interop/00001",
validatorPubKey: hexToBytes("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b"),
withdrawalCredentials: hexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"),
validatorPubKey: validatorPubKey2,
withdrawalCredentials: testutil.HexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"),
amount: 32000000000,
signature: hexToBytes("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab"),
depositMessageRoot: hexToBytes("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52"),
signature: signature2,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot2,
depositMessageRoot: depositMessageRoot2,
},
},
res: `["0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001209e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a35540000000000000000000000000000000000000000000000000000000000000030a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b0000000000000000000000000000000000000000000000000000000000000060b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001203b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab0000000000000000000000000000000000000000000000000000000000000030b89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f35940000000000000000000000000000000000000000000000000000000000000060911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e"]`,

View File

@@ -14,6 +14,7 @@
package depositdata
import (
"context"
"fmt"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
@@ -41,43 +42,45 @@ func process(data *dataIn) ([]*dataOut, error) {
depositMessage := &spec.DepositMessage{
PublicKey: pubKey,
WithdrawalCredentials: data.withdrawalCredentials,
Amount: spec.Gwei(data.amount),
Amount: data.amount,
}
depositMessageRoot, err := depositMessage.HashTreeRoot()
root, err := depositMessage.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to generate deposit message root")
}
var depositMessageRoot spec.Root
copy(depositMessageRoot[:], root[:])
sig, err := signing.SignRoot(validatorAccount, depositMessageRoot[:], data.domain)
sig, err := signing.SignRoot(context.Background(), validatorAccount, data.passphrases, depositMessageRoot, *data.domain)
if err != nil {
return nil, errors.Wrap(err, "failed to sign deposit message")
}
var signature spec.BLSSignature
copy(signature[:], sig)
depositData := &spec.DepositData{
PublicKey: pubKey,
WithdrawalCredentials: data.withdrawalCredentials,
Amount: spec.Gwei(data.amount),
Signature: signature,
Amount: data.amount,
Signature: sig,
}
depositDataRoot, err := depositData.HashTreeRoot()
root, err = depositData.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to generate deposit data root")
}
var depositDataRoot spec.Root
copy(depositDataRoot[:], root[:])
validatorWallet := validatorAccount.(e2wtypes.AccountWalletProvider).Wallet()
results = append(results, &dataOut{
format: data.format,
account: fmt.Sprintf("%s/%s", validatorWallet.Name(), validatorAccount.Name()),
validatorPubKey: validatorPubKey.Marshal(),
validatorPubKey: &pubKey,
withdrawalCredentials: data.withdrawalCredentials,
amount: data.amount,
signature: sig,
signature: &sig,
forkVersion: data.forkVersion,
depositMessageRoot: depositMessageRoot[:],
depositDataRoot: depositDataRoot[:],
depositMessageRoot: &depositMessageRoot,
depositDataRoot: &depositDataRoot,
})
}
return results, nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019, 2020 eald 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
@@ -17,8 +17,10 @@ import (
"context"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
@@ -36,17 +38,69 @@ func TestProcess(t *testing.T) {
viper.Set("passphrase", "pass")
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
interop1, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 1",
hexToBytes("0x51d0b65185db6989ab0b560d6deed19c7ead0e24b9b6372cbecb1f26bdfad000"),
testutil.HexToBytes("0x51d0b65185db6989ab0b560d6deed19c7ead0e24b9b6372cbecb1f26bdfad000"),
[]byte("pass"),
)
require.NoError(t, err)
var validatorPubKey *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c")
validatorPubKey = &tmp
}
var signature *spec.BLSSignature
{
tmp := testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2")
signature = &tmp
}
var forkVersion *spec.Version
{
tmp := testutil.HexToVersion("0x01020304")
forkVersion = &tmp
}
var domain *spec.Domain
{
tmp := testutil.HexToDomain("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0")
domain = &tmp
}
var depositDataRoot *spec.Root
{
tmp := testutil.HexToRoot("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554")
depositDataRoot = &tmp
}
var depositMessageRoot *spec.Root
{
tmp := testutil.HexToRoot("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6")
depositMessageRoot = &tmp
}
var validatorPubKey2 *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b")
validatorPubKey2 = &tmp
}
var signature2 *spec.BLSSignature
{
tmp := testutil.HexToSignature("0x939aedb76236c971c21227189c6a3a40d07909d19999798490294d284130a913b6f91d41d875768fb3e2ea4dcec672a316e5951272378f5df80a7c34fadb9a4d8462ee817faf50fe8b1c33e72d884fb17e71e665724f9e17bdf11f48eb6e9bfd")
signature2 = &tmp
}
var depositDataRoot2 *spec.Root
{
tmp := testutil.HexToRoot("0x182c7708aad7027bea2f6251eddf62431fae4876ee3e55339082219ae7014443")
depositDataRoot2 = &tmp
}
var depositMessageRoot2 *spec.Root
{
tmp := testutil.HexToRoot("0x1dc5053486d74f5c91fa90e1e86d718d3fb42bb92e5cfdce98e994eb2bff2c46")
depositMessageRoot2 = &tmp
}
tests := []struct {
name string
dataIn *dataIn
@@ -61,23 +115,24 @@ func TestProcess(t *testing.T) {
name: "Single",
dataIn: &dataIn{
format: "raw",
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
passphrases: []string{"pass"},
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: hexToBytes("0x01020304"),
domain: hexToBytes("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0"),
forkVersion: forkVersion,
domain: domain,
},
res: []*dataOut{
{
format: "raw",
account: "Test/Interop 0",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
validatorPubKey: validatorPubKey,
amount: 32000000000,
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
},
@@ -85,34 +140,35 @@ func TestProcess(t *testing.T) {
name: "Double",
dataIn: &dataIn{
format: "raw",
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
passphrases: []string{"pass"},
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0, interop1},
forkVersion: hexToBytes("0x01020304"),
domain: hexToBytes("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0"),
forkVersion: forkVersion,
domain: domain,
},
res: []*dataOut{
{
format: "raw",
account: "Test/Interop 0",
validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
validatorPubKey: validatorPubKey,
amount: 32000000000,
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"),
depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"),
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: signature,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
{
format: "raw",
account: "Test/Interop 1",
validatorPubKey: hexToBytes("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b"),
validatorPubKey: validatorPubKey2,
amount: 32000000000,
withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: hexToBytes("0x939aedb76236c971c21227189c6a3a40d07909d19999798490294d284130a913b6f91d41d875768fb3e2ea4dcec672a316e5951272378f5df80a7c34fadb9a4d8462ee817faf50fe8b1c33e72d884fb17e71e665724f9e17bdf11f48eb6e9bfd"),
forkVersion: hexToBytes("0x01020304"),
depositDataRoot: hexToBytes("0x182c7708aad7027bea2f6251eddf62431fae4876ee3e55339082219ae7014443"),
depositMessageRoot: hexToBytes("0x1dc5053486d74f5c91fa90e1e86d718d3fb42bb92e5cfdce98e994eb2bff2c46"),
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: signature2,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot2,
depositMessageRoot: depositMessageRoot2,
},
},
},

147
cmd/validator/exit/input.go Normal file
View File

@@ -0,0 +1,147 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexit
import (
"context"
"encoding/hex"
"encoding/json"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/core"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
// Operation.
eth2Client eth2client.Service
jsonOutput bool
// Chain information.
fork *spec.Fork
currentEpoch spec.Epoch
// Exit information.
account e2wtypes.Account
passphrases []string
epoch spec.Epoch
domain spec.Domain
signedVoluntaryExit *spec.SignedVoluntaryExit
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
data.passphrases = util.GetPassphrases()
data.jsonOutput = viper.GetBool("json")
switch {
case viper.GetString("exit") != "":
return inputJSON(ctx, data)
case viper.GetString("account") != "":
return inputAccount(ctx, data)
case viper.GetString("key") != "":
return inputKey(ctx, data)
default:
return nil, errors.New("must supply account, key, or pre-constructed JSON")
}
}
func inputJSON(ctx context.Context, data *dataIn) (*dataIn, error) {
validatorData := &util.ValidatorExitData{}
err := json.Unmarshal([]byte(viper.GetString("exit")), validatorData)
if err != nil {
return nil, err
}
data.signedVoluntaryExit = validatorData.Data
return inputChainData(ctx, data)
}
func inputAccount(ctx context.Context, data *dataIn) (*dataIn, error) {
var err error
_, data.account, err = core.WalletAndAccountFromInput(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain acount")
}
return inputChainData(ctx, data)
}
func inputKey(ctx context.Context, data *dataIn) (*dataIn, error) {
privKeyBytes, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode key")
}
data.account, err = util.NewScratchAccount(privKeyBytes, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to create acount from key")
}
return inputChainData(ctx, data)
}
func inputChainData(ctx context.Context, data *dataIn) (*dataIn, error) {
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
// Current fork.
data.fork, err = data.eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain fork information")
}
// Calculate current epoch.
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
}
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
}
data.currentEpoch = spec.Epoch(uint64(time.Since(genesis.GenesisTime).Seconds()) / (uint64(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()) * config["SLOTS_PER_EPOCH"].(uint64)))
// Epoch.
if viper.GetInt64("epoch") == -1 {
data.epoch = data.currentEpoch
} else {
data.epoch = spec.Epoch(viper.GetUint64("epoch"))
}
// Domain.
domain, err := data.eth2Client.(eth2client.DomainProvider).Domain(ctx, config["DOMAIN_VOLUNTARY_EXIT"].(spec.DomainType), data.epoch)
if err != nil {
return nil, errors.New("failed to calculate domain")
}
data.domain = domain
return data, nil
}

View File

@@ -0,0 +1,186 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexit
import (
"context"
"os"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"account": "Test wallet",
"wallet-passphrase": "ce%NohGhah4ye5ra",
"type": "nd",
},
err: "timeout is required",
},
{
name: "NoMethod",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "must supply account, key, or pre-constructed JSON",
},
{
name: "KeyInvalid",
vars: map[string]interface{}{
"timeout": "5s",
"key": "0xinvalid",
},
err: "failed to decode key: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "KeyBad",
vars: map[string]interface{}{
"timeout": "5s",
"key": "0x00",
},
err: "failed to create acount from key: private key must be 32 bytes",
},
{
name: "KeyGood",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
{
name: "AccountUnknown",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"account": "Test wallet/unknown",
},
res: &dataIn{
timeout: 5 * time.Second,
},
err: "failed to obtain acount: failed to obtain account: no account with name \"unknown\"",
},
{
name: "AccountGood",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"account": "Test wallet/Interop 0",
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
{
name: "JSONInvalid",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"exit": `invalid`,
},
res: &dataIn{
timeout: 5 * time.Second,
},
err: "invalid character 'i' looking for beginning of value",
},
{
name: "JSONGood",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"exit": `{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"}`,
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
{
name: "ClientBad",
vars: map[string]interface{}{
"connection": "localhost:1",
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
{
name: "EpochProvided",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
"epoch": "123",
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
}
})
}
}

View File

@@ -0,0 +1,57 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexit
import (
"context"
"encoding/json"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
)
type dataOut struct {
jsonOutput bool
forkVersion spec.Version
signedVoluntaryExit *spec.SignedVoluntaryExit
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.signedVoluntaryExit == nil {
return "", errors.New("no signed voluntary exit")
}
if data.jsonOutput {
return outputJSON(ctx, data)
}
return "", nil
}
func outputJSON(ctx context.Context, data *dataOut) (string, error) {
validatorExitData := &util.ValidatorExitData{
Data: data.signedVoluntaryExit,
ForkVersion: data.forkVersion,
}
bytes, err := json.Marshal(validatorExitData)
if err != nil {
return "", errors.Wrap(err, "failed to generate JSON")
}
return string(bytes), nil
}

View File

@@ -0,0 +1,97 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexit
import (
"context"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "SignedVoluntaryExitNil",
dataOut: &dataOut{
jsonOutput: true,
},
err: "no signed voluntary exit",
},
{
name: "Good",
dataOut: &dataOut{
forkVersion: spec.Version{0x01, 0x02, 0x03, 0x04},
signedVoluntaryExit: &spec.SignedVoluntaryExit{
Message: &spec.VoluntaryExit{
Epoch: spec.Epoch(123),
ValidatorIndex: spec.ValidatorIndex(456),
},
Signature: spec.BLSSignature{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
},
},
},
},
{
name: "JSON",
dataOut: &dataOut{
jsonOutput: true,
forkVersion: spec.Version{0x01, 0x02, 0x03, 0x04},
signedVoluntaryExit: &spec.SignedVoluntaryExit{
Message: &spec.VoluntaryExit{
Epoch: spec.Epoch(123),
ValidatorIndex: spec.ValidatorIndex(456),
},
Signature: spec.BLSSignature{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
},
},
},
res: `{"data":{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"},"fork_version":"0x01020304"}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

View File

@@ -0,0 +1,133 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexit
import (
"context"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/core"
"github.com/wealdtech/ethdo/signing"
)
// maxFutureEpochs is the farthest in the future for which an exit will be created.
var maxFutureEpochs = spec.Epoch(1024)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.epoch > data.currentEpoch {
if data.epoch-data.currentEpoch > maxFutureEpochs {
return nil, errors.New("not generating exit for an epoch in the far future")
}
}
results := &dataOut{
forkVersion: data.fork.CurrentVersion,
jsonOutput: data.jsonOutput,
}
validator, err := fetchValidator(ctx, data)
if err != nil {
return nil, err
}
exit, err := generateExit(ctx, data, validator)
if err != nil {
return nil, errors.Wrap(err, "failed to generate voluntary exit")
}
root, err := exit.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to generate root for voluntary exit")
}
if data.account != nil {
signature, err := signing.SignRoot(ctx, data.account, data.passphrases, root, data.domain)
if err != nil {
return nil, errors.Wrap(err, "failed to sign voluntary exit")
}
results.signedVoluntaryExit = &spec.SignedVoluntaryExit{
Message: exit,
Signature: signature,
}
} else {
results.signedVoluntaryExit = data.signedVoluntaryExit
}
if !data.jsonOutput {
if err := broadcastExit(ctx, data, results); err != nil {
return nil, errors.Wrap(err, "failed to broadcast voluntary exit")
}
}
return results, nil
}
func generateExit(ctx context.Context, data *dataIn, validator *api.Validator) (*spec.VoluntaryExit, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.signedVoluntaryExit != nil {
return data.signedVoluntaryExit.Message, nil
}
if validator == nil {
return nil, errors.New("no validator")
}
exit := &spec.VoluntaryExit{
Epoch: data.epoch,
ValidatorIndex: validator.Index,
}
return exit, nil
}
func broadcastExit(ctx context.Context, data *dataIn, results *dataOut) error {
return data.eth2Client.(eth2client.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, results.signedVoluntaryExit)
}
func fetchValidator(ctx context.Context, data *dataIn) (*api.Validator, error) {
// Validator.
if data.account == nil {
return nil, nil
}
var validator *api.Validator
validatorPubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := core.BestPublicKey(data.account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
}
copy(validatorPubKeys[0][:], pubKey.Marshal())
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, "head", validatorPubKeys)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator from beacon node")
}
if len(validators) == 0 {
return nil, errors.New("validator not known by beacon node")
}
for _, v := range validators {
validator = v
}
if validator.Status != api.ValidatorStateActiveOngoing {
return nil, errors.New("validator is not active; cannot exit")
}
return validator, nil
}

View File

@@ -0,0 +1,234 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexit
import (
"context"
"os"
"testing"
"time"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/auto"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
eth2Client, err := auto.New(context.Background(),
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
)
require.NoError(t, err)
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
// activeValidator := &api.Validator{
// Index: 123,
// Balance: 32123456789,
// Status: api.ValidatorStateActiveOngoing,
// Validator: &spec.Validator{
// PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
// WithdrawalCredentials: nil,
// EffectiveBalance: 32000000000,
// Slashed: false,
// ActivationEligibilityEpoch: 0,
// ActivationEpoch: 0,
// ExitEpoch: 0,
// WithdrawableEpoch: 0,
// },
// }
epochFork := &spec.Fork{
PreviousVersion: spec.Version{0x00, 0x00, 0x00, 0x00},
CurrentVersion: spec.Version{0x00, 0x00, 0x00, 0x00},
Epoch: 0,
}
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "EpochTooLate",
dataIn: &dataIn{
timeout: 5 * time.Second,
eth2Client: eth2Client,
fork: epochFork,
currentEpoch: 10,
account: interop0,
passphrases: []string{"pass"},
epoch: 9999999,
domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
},
err: "not generating exit for an epoch in the far future",
},
{
name: "AccountUnknown",
dataIn: &dataIn{
timeout: 5 * time.Second,
eth2Client: eth2Client,
fork: epochFork,
currentEpoch: 10,
account: interop0,
passphrases: []string{"pass"},
epoch: 10,
domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
},
err: "validator not known by beacon node",
},
// {
// name: "Good",
// dataIn: &dataIn{
// timeout: 5 * time.Second,
// eth2Client: eth2Client,
// fork: epochFork,
// currentEpoch: 10,
// account: interop0,
// passphrases: []string{"pass"},
// epoch: 10,
// domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
// },
// },
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}
func TestGenerateExit(t *testing.T) {
activeValidator := &api.Validator{
Index: 123,
Balance: 32123456789,
Status: api.ValidatorStateActiveOngoing,
Validator: &spec.Validator{
PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
WithdrawalCredentials: nil,
EffectiveBalance: 32000000000,
Slashed: false,
ActivationEligibilityEpoch: 0,
ActivationEpoch: 0,
ExitEpoch: 0,
WithdrawableEpoch: 0,
},
}
tests := []struct {
name string
validator *api.Validator
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "SignedVoluntaryExitGood",
dataIn: &dataIn{
signedVoluntaryExit: &spec.SignedVoluntaryExit{
Message: &spec.VoluntaryExit{
Epoch: spec.Epoch(123),
ValidatorIndex: spec.ValidatorIndex(456),
},
Signature: spec.BLSSignature{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
},
},
},
},
{
name: "ValidatorMissing",
dataIn: &dataIn{},
err: "no validator",
},
{
name: "ValidatorGood",
dataIn: &dataIn{},
validator: activeValidator,
},
{
name: "Good",
dataIn: &dataIn{
signedVoluntaryExit: &spec.SignedVoluntaryExit{
Message: &spec.VoluntaryExit{
Epoch: spec.Epoch(123),
ValidatorIndex: spec.ValidatorIndex(456),
},
Signature: spec.BLSSignature{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
},
},
},
validator: activeValidator,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := generateExit(context.Background(), test.dataIn, test.validator)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

50
cmd/validator/exit/run.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright © 2019, 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexit
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the wallet create data command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

@@ -14,29 +14,13 @@
package cmd
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/grpc"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
validatorexit "github.com/wealdtech/ethdo/cmd/validator/exit"
)
var validatorExitEpoch int64
var validatorExitKey string
var validatorExitJSON string
var validatorExitJSONOutput bool
var validatorExitCmd = &cobra.Command{
Use: "exit",
Short: "Send an exit request for a validator",
@@ -45,219 +29,41 @@ var validatorExitCmd = &cobra.Command{
ethdo validator exit --account=primary/validator --passphrase=secret
In quiet mode this will return 0 if the transaction has been generated, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
err := connect()
errCheck(err, "Failed to obtain connect to Ethereum 2 beacon chain node")
exit, signature, forkVersion := validatorExitHandleInput(ctx)
validatorExitHandleExit(ctx, exit, signature, forkVersion)
os.Exit(_exitSuccess)
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorexit.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func validatorExitHandleInput(ctx context.Context) (*ethpb.VoluntaryExit, e2types.Signature, []byte) {
if validatorExitJSON != "" {
return validatorExitHandleJSONInput(validatorExitJSON)
}
if viper.GetString("account") != "" {
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
outputIf(debug, fmt.Sprintf("Account %s obtained", account.Name()))
return validatorExitHandleAccountInput(ctx, account)
}
if validatorExitKey != "" {
privKeyBytes, err := hex.DecodeString(strings.TrimPrefix(validatorExitKey, "0x"))
errCheck(err, fmt.Sprintf("Failed to decode key %s", validatorExitKey))
account, err := util.NewScratchAccount(privKeyBytes, nil)
errCheck(err, "Invalid private key")
return validatorExitHandleAccountInput(ctx, account)
}
die("one of --json, --account or --key is required")
return nil, nil, nil
}
func validatorExitHandleJSONInput(input string) (*ethpb.VoluntaryExit, e2types.Signature, []byte) {
data := &validatorExitData{}
err := json.Unmarshal([]byte(input), data)
errCheck(err, "Invalid JSON input")
exit := &ethpb.VoluntaryExit{
Epoch: data.Epoch,
ValidatorIndex: data.ValidatorIndex,
}
signature, err := e2types.BLSSignatureFromBytes(data.Signature)
errCheck(err, "Invalid signature")
return exit, signature, data.ForkVersion
}
func validatorExitHandleAccountInput(ctx context.Context, account e2wtypes.Account) (*ethpb.VoluntaryExit, e2types.Signature, []byte) {
exit := &ethpb.VoluntaryExit{}
// Beacon chain config required for later work.
config, err := grpc.FetchChainConfig(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain configuration")
secondsPerSlot, ok := config["SecondsPerSlot"].(uint64)
assert(ok, "Failed to obtain seconds per slot from chain")
slotsPerEpoch, ok := config["SlotsPerEpoch"].(uint64)
assert(ok, "Failed to obtain slots per epoch from chain")
secondsPerEpoch := secondsPerSlot * slotsPerEpoch
// Fetch the validator's index.
index, err := grpc.FetchValidatorIndex(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator index")
outputIf(debug, fmt.Sprintf("Validator index is %d", index))
exit.ValidatorIndex = index
// Ensure the validator is active.
state, err := grpc.FetchValidatorState(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator state")
outputIf(debug, fmt.Sprintf("Validator state is %v", state))
assert(state == ethpb.ValidatorStatus_ACTIVE, "Validator must be active to exit")
if validatorExitEpoch < 0 {
// Ensure the validator has been active long enough to exit.
validator, err := grpc.FetchValidator(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator information")
outputIf(debug, fmt.Sprintf("Activation epoch is %v", validator.ActivationEpoch))
shardCommitteePeriod, ok := config["ShardCommitteePeriod"].(uint64)
assert(ok, "Failed to obtain shard committee period from chain")
earliestExitEpoch := validator.ActivationEpoch + shardCommitteePeriod
genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn)
errCheck(err, "Failed to obtain genesis time")
currentEpoch := uint64(time.Since(genesisTime).Seconds()) / secondsPerEpoch
assert(currentEpoch >= earliestExitEpoch, fmt.Sprintf("Validator cannot exit until %s ( epoch %d); transaction not sent", genesisTime.Add(time.Duration(secondsPerEpoch*earliestExitEpoch)*time.Second).Format(time.UnixDate), earliestExitEpoch))
outputIf(verbose, "Validator confirmed to be in a suitable state")
exit.Epoch = currentEpoch
} else {
// User-specified epoch; no checks.
exit.Epoch = uint64(validatorExitEpoch)
}
// TODO fetch current fork version from config (currently using genesis fork version)
forkVersion := config["GenesisForkVersion"].([]byte)
outputIf(debug, fmt.Sprintf("Current fork version is %x", forkVersion))
genesisValidatorsRoot, err := grpc.FetchGenesisValidatorsRoot(eth2GRPCConn)
outputIf(debug, fmt.Sprintf("Genesis validators root is %x", genesisValidatorsRoot))
errCheck(err, "Failed to obtain genesis validators root")
domain := e2types.Domain(e2types.DomainVoluntaryExit, forkVersion, genesisValidatorsRoot)
alreadyUnlocked, err := unlock(account)
errCheck(err, "Failed to unlock account; please confirm passphrase is correct")
signature, err := signStruct(account, exit, domain)
if !alreadyUnlocked {
errCheck(lock(account), "Failed to re-lock account")
}
errCheck(err, "Failed to sign exit proposal")
return exit, signature, forkVersion
}
// validatorExitHandleExit handles the exit request.
func validatorExitHandleExit(ctx context.Context, exit *ethpb.VoluntaryExit, signature e2types.Signature, forkVersion []byte) {
if validatorExitJSONOutput {
data := &validatorExitData{
Epoch: exit.Epoch,
ValidatorIndex: exit.ValidatorIndex,
Signature: signature.Marshal(),
ForkVersion: forkVersion,
}
res, err := json.Marshal(data)
errCheck(err, "Failed to generate JSON")
outputIf(!quiet, string(res))
} else {
proposal := &ethpb.SignedVoluntaryExit{
Exit: exit,
Signature: signature.Marshal(),
}
validatorClient := ethpb.NewBeaconNodeValidatorClient(eth2GRPCConn)
_, err := validatorClient.ProposeExit(ctx, proposal)
errCheck(err, "Failed to propose exit")
outputIf(!quiet, "Validator exit transaction sent")
}
}
func init() {
validatorCmd.AddCommand(validatorExitCmd)
validatorFlags(validatorExitCmd)
validatorExitCmd.Flags().Int64Var(&validatorExitEpoch, "epoch", -1, "Epoch at which to exit (defaults to current epoch)")
validatorExitCmd.Flags().StringVar(&validatorExitKey, "key", "", "Private key if account not known by ethdo")
validatorExitCmd.Flags().BoolVar(&validatorExitJSONOutput, "json-output", false, "Print JSON transaction; do not broadcast to network")
validatorExitCmd.Flags().StringVar(&validatorExitJSON, "json", "", "Use JSON as created by --json-output to exit")
validatorExitCmd.Flags().Int64("epoch", -1, "Epoch at which to exit (defaults to current epoch)")
validatorExitCmd.Flags().String("key", "", "Private key if validator not known by ethdo")
validatorExitCmd.Flags().String("exit", "", "Use pre-defined JSON data as created by --json to exit")
validatorExitCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
}
type validatorExitData struct {
Epoch uint64 `json:"epoch"`
ValidatorIndex uint64 `json:"validator_index"`
Signature []byte `json:"signature"`
ForkVersion []byte `json:"fork_version"`
}
// MarshalJSON implements custom JSON marshaller.
func (d *validatorExitData) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"epoch":%d,"validator_index":%d,"signature":"%#x","fork_version":"%#x"}`, d.Epoch, d.ValidatorIndex, d.Signature, d.ForkVersion)), nil
}
// UnmarshalJSON implements custom JSON unmarshaller.
func (d *validatorExitData) UnmarshalJSON(data []byte) error {
var v map[string]interface{}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
if val, exists := v["epoch"]; exists {
var ok bool
epoch, ok := val.(float64)
if !ok {
return errors.New("epoch invalid")
}
d.Epoch = uint64(epoch)
} else {
return errors.New("epoch missing")
}
if val, exists := v["validator_index"]; exists {
var ok bool
validatorIndex, ok := val.(float64)
if !ok {
return errors.New("validator_index invalid")
}
d.ValidatorIndex = uint64(validatorIndex)
} else {
return errors.New("validator_index missing")
}
if val, exists := v["signature"]; exists {
signatureBytes, ok := val.(string)
if !ok {
return errors.New("signature invalid")
}
signature, err := hex.DecodeString(strings.TrimPrefix(signatureBytes, "0x"))
if err != nil {
return errors.Wrap(err, "signature invalid")
}
d.Signature = signature
} else {
return errors.New("signature missing")
}
if val, exists := v["fork_version"]; exists {
forkVersionBytes, ok := val.(string)
if !ok {
return errors.New("fork version invalid")
}
forkVersion, err := hex.DecodeString(strings.TrimPrefix(forkVersionBytes, "0x"))
if err != nil {
return errors.Wrap(err, "fork version invalid")
}
d.ForkVersion = forkVersion
} else {
return errors.New("fork version missing")
}
return nil
func validatorExitBindings() {
if err := viper.BindPFlag("epoch", validatorExitCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("key", validatorExitCmd.Flags().Lookup("key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("exit", validatorExitCmd.Flags().Lookup("exit")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", validatorExitCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -24,20 +24,19 @@ import (
"os"
"strconv"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/grpc"
"github.com/wealdtech/ethdo/core"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
string2eth "github.com/wealdtech/go-string2eth"
)
var validatorInfoPubKey string
var validatorInfoCmd = &cobra.Command{
Use: "info",
Short: "Obtain information about a validator",
@@ -47,78 +46,81 @@ var validatorInfoCmd = &cobra.Command{
In quiet mode this will return 0 if the validator information can be obtained, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(viper.GetString("account") != "" || validatorInfoPubKey != "", "--account or --pubkey is required")
ctx := context.Background()
err := connect()
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node")
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")
account, err := validatorInfoAccount()
errCheck(err, "Failed to obtain validator account")
pubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := core.BestPublicKey(account)
errCheck(err, "Failed to obtain validator public key")
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, "head", pubKeys)
errCheck(err, "Failed to obtain validator information")
if len(validators) == 0 {
fmt.Println("Validator not known by beacon node")
os.Exit(_exitSuccess)
}
var validator *api.Validator
for _, v := range validators {
validator = v
}
if verbose {
network := network()
network, err := util.Network(ctx, eth2Client)
errCheck(err, "Failed to obtain network")
outputIf(debug, fmt.Sprintf("Network is %s", network))
pubKey, err := bestPublicKey(account)
if err == nil {
deposits, totalDeposited, err := graphData(network, pubKey.Marshal())
if err == nil {
fmt.Printf("Number of deposits: %d\n", deposits)
fmt.Printf("Total deposited: %s\n", string2eth.GWeiToString(totalDeposited, true))
fmt.Printf("Total deposited: %s\n", string2eth.GWeiToString(uint64(totalDeposited), true))
}
}
}
validatorInfo, err := grpc.FetchValidatorInfo(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator information")
validator, err := grpc.FetchValidator(eth2GRPCConn, account)
if err != nil {
// We can live with this.
validator = nil
}
if validatorInfo.Status != ethpb.ValidatorStatus_DEPOSITED &&
validatorInfo.Status != ethpb.ValidatorStatus_UNKNOWN_STATUS {
errCheck(err, "Failed to obtain validator definition")
}
assert(validatorInfo.Status != ethpb.ValidatorStatus_UNKNOWN_STATUS, "Not known as a validator")
if quiet {
os.Exit(_exitSuccess)
}
outputIf(verbose, fmt.Sprintf("Epoch of data: %v", validatorInfo.Epoch))
outputIf(verbose && validatorInfo.Status != ethpb.ValidatorStatus_DEPOSITED, fmt.Sprintf("Index: %v", validatorInfo.Index))
outputIf(verbose, fmt.Sprintf("Public key: %#x", validatorInfo.PublicKey))
fmt.Printf("Status: %s\n", strings.Title(strings.ToLower(validatorInfo.Status.String())))
fmt.Printf("Balance: %s\n", string2eth.GWeiToString(validatorInfo.Balance, true))
if validatorInfo.Status == ethpb.ValidatorStatus_ACTIVE ||
validatorInfo.Status == ethpb.ValidatorStatus_EXITING ||
validatorInfo.Status == ethpb.ValidatorStatus_SLASHING {
fmt.Printf("Effective balance: %s\n", string2eth.GWeiToString(validatorInfo.EffectiveBalance, true))
}
if validator != nil {
outputIf(verbose, fmt.Sprintf("Withdrawal credentials: %#x", validator.WithdrawalCredentials))
}
transition := time.Unix(int64(validatorInfo.TransitionTimestamp), 0)
transitionPassed := int64(validatorInfo.TransitionTimestamp) <= time.Now().Unix()
switch validatorInfo.Status {
case ethpb.ValidatorStatus_DEPOSITED:
if validatorInfo.TransitionTimestamp != 0 {
fmt.Printf("Inclusion in chain: %s\n", transition)
}
case ethpb.ValidatorStatus_PENDING:
fmt.Printf("Activation: %s\n", transition)
case ethpb.ValidatorStatus_EXITING, ethpb.ValidatorStatus_SLASHING:
fmt.Printf("Attesting finishes: %s\n", transition)
case ethpb.ValidatorStatus_EXITED:
if transitionPassed {
fmt.Printf("Funds withdrawable: Now\n")
} else {
fmt.Printf("Funds withdrawable: %s\n", transition)
if verbose {
if validator.Status.HasActivated() {
fmt.Printf("Index: %d\n", validator.Index)
}
fmt.Printf("Public key: %#x\n", validator.Validator.PublicKey)
}
fmt.Printf("Status: %v\n", validator.Status)
fmt.Printf("Balance: %s\n", string2eth.GWeiToString(uint64(validator.Balance), true))
if validator.Status.IsActive() {
fmt.Printf("Effective balance: %s\n", string2eth.GWeiToString(uint64(validator.Validator.EffectiveBalance), true))
}
if verbose {
fmt.Printf("Withdrawal credentials: %#x\n", validator.Validator.WithdrawalCredentials)
}
// transition := time.Unix(int64(validatorInfo.TransitionTimestamp), 0)
// transitionPassed := int64(validatorInfo.TransitionTimestamp) <= time.Now().Unix()
// switch validatorInfo.Status {
// case ethpb.ValidatorStatus_DEPOSITED:
// if validatorInfo.TransitionTimestamp != 0 {
// fmt.Printf("Inclusion in chain: %s\n", transition)
// }
// case ethpb.ValidatorStatus_PENDING:
// fmt.Printf("Activation: %s\n", transition)
// case ethpb.ValidatorStatus_EXITING, ethpb.ValidatorStatus_SLASHING:
// fmt.Printf("Attesting finishes: %s\n", transition)
// case ethpb.ValidatorStatus_EXITED:
// if transitionPassed {
// fmt.Printf("Funds withdrawable: Now\n")
// } else {
// fmt.Printf("Funds withdrawable: %s\n", transition)
// }
// }
os.Exit(_exitSuccess)
},
@@ -128,29 +130,37 @@ In quiet mode this will return 0 if the validator information can be obtained, o
func validatorInfoAccount() (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
switch {
case viper.GetString("account") != "":
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, account, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(validatorInfoPubKey, "0x"))
case viper.GetString("pubkey") != "":
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("pubkey"), "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", validatorInfoPubKey))
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", viper.GetString("pubkey")))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", validatorInfoPubKey))
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", viper.GetString("pubkey")))
}
default:
return nil, errors.New("neither account nor public key supplied")
}
return account, nil
}
// graphData returns data from the graph about number and amount of deposits
func graphData(network string, validatorPubKey []byte) (uint64, uint64, error) {
subgraph := fmt.Sprintf("attestantio/eth2deposits-%s", strings.ToLower(network))
func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error) {
subgraph := ""
if network == "Mainnet" {
subgraph = "attestantio/eth2deposits"
} else {
subgraph = fmt.Sprintf("attestantio/eth2deposits-%s", strings.ToLower(network))
}
query := fmt.Sprintf(`{"query": "{deposits(where: {validatorPubKey:\"%#x\"}) { id amount withdrawalCredentials }}"}`, validatorPubKey)
url := fmt.Sprintf("https://api.thegraph.com/subgraphs/name/%s", subgraph)
graphResp, err := http.Post(url, "application/json", bytes.NewBufferString(query))
@@ -180,7 +190,7 @@ func graphData(network string, validatorPubKey []byte) (uint64, uint64, error) {
return 0, 0, errors.Wrap(err, "invalid data returned from existing deposit check")
}
deposits := uint64(0)
totalDeposited := uint64(0)
totalDeposited := spec.Gwei(0)
if response.Data != nil && len(response.Data.Deposits) > 0 {
for _, deposit := range response.Data.Deposits {
deposits++
@@ -188,7 +198,7 @@ func graphData(network string, validatorPubKey []byte) (uint64, uint64, error) {
if err != nil {
return 0, 0, errors.Wrap(err, fmt.Sprintf("invalid deposit amount from pre-existing deposit %s", deposit.Amount))
}
totalDeposited += depositAmount
totalDeposited += spec.Gwei(depositAmount)
}
}
return deposits, totalDeposited, nil
@@ -196,6 +206,12 @@ func graphData(network string, validatorPubKey []byte) (uint64, uint64, error) {
func init() {
validatorCmd.AddCommand(validatorInfoCmd)
validatorInfoCmd.Flags().StringVar(&validatorInfoPubKey, "pubkey", "", "Public key for which to obtain status")
validatorInfoCmd.Flags().String("pubkey", "", "Public key for which to obtain status")
validatorFlags(validatorInfoCmd)
}
func validatorInfoBindings() {
if err := viper.BindPFlag("pubkey", validatorInfoCmd.Flags().Lookup("pubkey")); err != nil {
panic(err)
}
}

View File

@@ -385,9 +385,8 @@ Validator commands focus on interaction with Ethereum 2 validators.
`ethdo validator exit` sends a transaction to the chain to tell an active validator to exit the validation queue. Options include:
- `epoch` specify an epoch before which this exit is not valid
- `json-output` generate JSON output rather than sending a transaction immediately
- `json` use JSON input created by the `--json-output` option rather than generate data from scratch
- `forkversion` specify a specific fork version; default is to fetch it from the chain but this can be used when generating offline deposits
- `json` generate JSON output rather than sending a transaction immediately
- `exit` use JSON exit input created by the `--json` option rather than generate data from scratch
```sh
$ ethdo validator exit --account=Validators/1 --passphrase="my validator secret"

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.13
require (
github.com/OneOfOne/xxhash v1.2.5 // indirect
github.com/attestantio/dirk v0.9.2
github.com/attestantio/go-eth2-client v0.6.9
github.com/attestantio/go-eth2-client v0.6.10
github.com/ferranbt/fastssz v0.0.0-20201030134205-9b9624098321
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gofrs/uuid v3.3.0+incompatible

4
go.sum
View File

@@ -71,6 +71,8 @@ github.com/attestantio/go-eth2-client v0.6.8 h1:Lsjx5P0pB8ruZBfJUbqy5hpevD4Zt8Z0
github.com/attestantio/go-eth2-client v0.6.8/go.mod h1:lYEayGHzZma9HMUJgyxFIzDWRck8n2IedP7KTkIwe0g=
github.com/attestantio/go-eth2-client v0.6.9 h1:Hbf4tX9MvxCsLokED8Ic3tQxmEAb/phoBkBmk8sKJm0=
github.com/attestantio/go-eth2-client v0.6.9/go.mod h1:ODAZ4yS1YYYew/EsgGsVb/siNEoa505CrGsvlVFdkfo=
github.com/attestantio/go-eth2-client v0.6.10 h1:PMNBMLk6xfMEUqhaUnsI0/HZRrstZF18Gt6Dm5GelW4=
github.com/attestantio/go-eth2-client v0.6.10/go.mod h1:ODAZ4yS1YYYew/EsgGsVb/siNEoa505CrGsvlVFdkfo=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.32.6 h1:HoswAabUWgnrUF7X/9dr4WRgrr8DyscxXvTDm7Qw/5c=
@@ -510,6 +512,7 @@ github.com/prysmaticlabs/go-bitfield v0.0.0-20200618145306-2ae0807bef65/go.mod h
github.com/prysmaticlabs/go-ssz v0.0.0-20200101200214-e24db4d9e963/go.mod h1:VecIJZrewdAuhVckySLFt2wAAHRME934bSDurP8ftkc=
github.com/prysmaticlabs/go-ssz v0.0.0-20200612203617-6d5c9aa213ae h1:7qd0Af1ozWKBU3c93YW2RH+/09hJns9+ftqWUZyts9c=
github.com/prysmaticlabs/go-ssz v0.0.0-20200612203617-6d5c9aa213ae/go.mod h1:VecIJZrewdAuhVckySLFt2wAAHRME934bSDurP8ftkc=
github.com/r3labs/sse/v2 v2.3.0 h1:R/UMa0ML6AYKQ8irQNHhY+204lz1LytDIdKhCxSVAd8=
github.com/r3labs/sse/v2 v2.3.0/go.mod h1:hUrYMKfu9WquG9MyI0r6TKiNH+6Sw/QPKm2YbNbU5g8=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@@ -1139,6 +1142,7 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -1,318 +0,0 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grpc
import (
"context"
"strconv"
"strings"
"github.com/gogo/protobuf/types"
"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/spf13/viper"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
"google.golang.org/grpc"
)
// FetchChainConfig fetches the chain configuration from the beacon node.
// It tweaks the output to make it easier to work with by setting appropriate
// types.
func FetchChainConfig(conn *grpc.ClientConn) (map[string]interface{}, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
config, err := beaconClient.GetBeaconConfig(ctx, &types.Empty{})
if err != nil {
return nil, err
}
results := make(map[string]interface{})
for k, v := range config.Config {
// Handle integers
if v == "0" {
results[k] = uint64(0)
continue
}
intVal, err := strconv.ParseUint(v, 10, 64)
if err == nil && intVal != 0 {
results[k] = intVal
continue
}
// Handle byte arrays
if strings.HasPrefix(v, "[") {
vals := strings.Split(v[1:len(v)-1], " ")
res := make([]byte, len(vals))
for i, val := range vals {
intVal, err := strconv.Atoi(val)
if err != nil {
return nil, errors.Wrapf(err, "failed to convert value %q for %s", v, k)
}
res[i] = byte(intVal)
}
results[k] = res
continue
}
// String (or unhandled format)
results[k] = v
}
return results, nil
}
func FetchLatestFilledSlot(conn *grpc.ClientConn) (uint64, error) {
if conn == nil {
return 0, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
chainHead, err := beaconClient.GetChainHead(ctx, &types.Empty{})
if err != nil {
return 0, errors.Wrap(err, "failed to obtain latest")
}
return chainHead.HeadSlot, nil
}
// FetchValidatorCommittees fetches the validator committees for a given epoch.
func FetchValidatorCommittees(conn *grpc.ClientConn, epoch uint64) (map[uint64][][]uint64, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
req := &ethpb.ListCommitteesRequest{
QueryFilter: &ethpb.ListCommitteesRequest_Epoch{
Epoch: epoch,
},
}
resp, err := beaconClient.ListBeaconCommittees(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain committees")
}
res := make(map[uint64][][]uint64)
for slot, committees := range resp.Committees {
res[slot] = make([][]uint64, len(resp.Committees))
for i, committee := range committees.Committees {
res[slot][uint64(i)] = make([]uint64, len(committee.ValidatorIndices))
indices := make([]uint64, len(committee.ValidatorIndices))
copy(indices, committee.ValidatorIndices)
res[slot][uint64(i)] = indices
}
}
return res, nil
}
// FetchValidator fetches the validator definition from the beacon node.
func FetchValidator(conn *grpc.ClientConn, account e2wtypes.Account) (*ethpb.Validator, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
var pubKey []byte
if pubKeyProvider, ok := account.(e2wtypes.AccountCompositePublicKeyProvider); ok {
pubKey = pubKeyProvider.CompositePublicKey().Marshal()
} else if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
pubKey = pubKeyProvider.PublicKey().Marshal()
} else {
return nil, errors.New("Unable to obtain public key")
}
req := &ethpb.GetValidatorRequest{
QueryFilter: &ethpb.GetValidatorRequest_PublicKey{
PublicKey: pubKey,
},
}
return beaconClient.GetValidator(ctx, req)
}
// FetchValidatorByIndex fetches the validator definition from the beacon node.
func FetchValidatorByIndex(conn *grpc.ClientConn, index uint64) (*ethpb.Validator, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
req := &ethpb.GetValidatorRequest{
QueryFilter: &ethpb.GetValidatorRequest_Index{
Index: index,
},
}
return beaconClient.GetValidator(ctx, req)
}
// FetchValidatorBalance fetches the validator balance from the beacon node.
func FetchValidatorBalance(conn *grpc.ClientConn, account e2wtypes.Account) (uint64, error) {
if conn == nil {
return 0, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
var pubKey []byte
if pubKeyProvider, ok := account.(e2wtypes.AccountCompositePublicKeyProvider); ok {
pubKey = pubKeyProvider.CompositePublicKey().Marshal()
} else if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
pubKey = pubKeyProvider.PublicKey().Marshal()
} else {
return 0, errors.New("Unable to obtain public key")
}
res, err := beaconClient.ListValidatorBalances(ctx, &ethpb.ListValidatorBalancesRequest{
PublicKeys: [][]byte{pubKey},
})
if err != nil {
return 0, err
}
if len(res.Balances) == 0 {
return 0, errors.New("unknown validator")
}
return res.Balances[0].Balance, nil
}
// FetchValidatorPerformance fetches the validator performance from the beacon node.
func FetchValidatorPerformance(conn *grpc.ClientConn, account e2wtypes.Account) (bool, bool, bool, uint64, int64, error) {
if conn == nil {
return false, false, false, 0, 0, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
var pubKey []byte
if pubKeyProvider, ok := account.(e2wtypes.AccountCompositePublicKeyProvider); ok {
pubKey = pubKeyProvider.CompositePublicKey().Marshal()
} else if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
pubKey = pubKeyProvider.PublicKey().Marshal()
} else {
return false, false, false, 0, 0, errors.New("Unable to obtain public key")
}
req := &ethpb.ValidatorPerformanceRequest{
PublicKeys: [][]byte{pubKey},
}
res, err := beaconClient.GetValidatorPerformance(ctx, req)
if err != nil {
return false, false, false, 0, 0, err
}
if len(res.InclusionDistances) == 0 {
return false, false, false, 0, 0, errors.New("unknown validator")
}
return res.CorrectlyVotedHead[0],
res.CorrectlyVotedSource[0],
res.CorrectlyVotedTarget[0],
res.InclusionDistances[0],
int64(res.BalancesAfterEpochTransition[0]) - int64(res.BalancesBeforeEpochTransition[0]),
err
}
// FetchValidatorInfo fetches current validator info from the beacon node.
func FetchValidatorInfo(conn *grpc.ClientConn, account e2wtypes.Account) (*ethpb.ValidatorInfo, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
stream, err := beaconClient.StreamValidatorsInfo(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to contact beacon node")
}
var pubKey []byte
if pubKeyProvider, ok := account.(e2wtypes.AccountCompositePublicKeyProvider); ok {
pubKey = pubKeyProvider.CompositePublicKey().Marshal()
} else if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
pubKey = pubKeyProvider.PublicKey().Marshal()
} else {
return nil, errors.New("Unable to obtain public key")
}
changeSet := &ethpb.ValidatorChangeSet{
Action: ethpb.SetAction_SET_VALIDATOR_KEYS,
PublicKeys: [][]byte{pubKey},
}
err = stream.Send(changeSet)
if err != nil {
return nil, errors.Wrap(err, "failed to send validator public key")
}
return stream.Recv()
}
// FetchChainInfo fetches current chain info from the beacon node.
func FetchChainInfo(conn *grpc.ClientConn) (*ethpb.ChainHead, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
return beaconClient.GetChainHead(ctx, &types.Empty{})
}
// FetchBlock fetches a block at a given slot from the beacon node.
func FetchBlock(conn *grpc.ClientConn, slot uint64) (*ethpb.SignedBeaconBlock, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
req := &ethpb.ListBlocksRequest{}
if slot == 0 {
req.QueryFilter = &ethpb.ListBlocksRequest_Genesis{Genesis: true}
} else {
req.QueryFilter = &ethpb.ListBlocksRequest_Slot{Slot: slot}
}
resp, err := beaconClient.ListBlocks(ctx, req)
if err != nil {
return nil, err
}
if len(resp.BlockContainers) == 0 {
return nil, nil
}
return resp.BlockContainers[0].Block, nil
}
func StreamBlocks(conn *grpc.ClientConn) (ethpb.BeaconChain_StreamBlocksClient, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
beaconClient := ethpb.NewBeaconChainClient(conn)
stream, err := beaconClient.StreamBlocks(context.Background(), &types.Empty{})
if err != nil {
return nil, err
}
return stream, nil
}

View File

@@ -1,85 +0,0 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grpc
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/viper"
"google.golang.org/grpc"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// FetchValidatorIndex fetches the index of a validator.
func FetchValidatorIndex(conn *grpc.ClientConn, account wtypes.Account) (uint64, error) {
if conn == nil {
return 0, errors.New("no connection to beacon node")
}
validatorClient := ethpb.NewBeaconNodeValidatorClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
var pubKey []byte
if pubKeyProvider, ok := account.(wtypes.AccountCompositePublicKeyProvider); ok {
pubKey = pubKeyProvider.CompositePublicKey().Marshal()
} else if pubKeyProvider, ok := account.(wtypes.AccountPublicKeyProvider); ok {
pubKey = pubKeyProvider.PublicKey().Marshal()
} else {
return 0, errors.New("Unable to obtain public key")
}
// Fetch the account.
req := &ethpb.ValidatorIndexRequest{
PublicKey: pubKey,
}
resp, err := validatorClient.ValidatorIndex(ctx, req)
if err != nil {
return 0, err
}
return resp.Index, nil
}
// FetchValidatorState fetches the state of a validator.
func FetchValidatorState(conn *grpc.ClientConn, account wtypes.Account) (ethpb.ValidatorStatus, error) {
if conn == nil {
return ethpb.ValidatorStatus_UNKNOWN_STATUS, errors.New("no connection to beacon node")
}
validatorClient := ethpb.NewBeaconNodeValidatorClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
var pubKey []byte
if pubKeyProvider, ok := account.(wtypes.AccountCompositePublicKeyProvider); ok {
pubKey = pubKeyProvider.CompositePublicKey().Marshal()
} else if pubKeyProvider, ok := account.(wtypes.AccountPublicKeyProvider); ok {
pubKey = pubKeyProvider.PublicKey().Marshal()
} else {
return ethpb.ValidatorStatus_UNKNOWN_STATUS, errors.New("Unable to obtain public key")
}
// Fetch the account.
req := &ethpb.ValidatorStatusRequest{
PublicKey: pubKey,
}
resp, err := validatorClient.ValidatorStatus(ctx, req)
if err != nil {
return ethpb.ValidatorStatus_UNKNOWN_STATUS, err
}
return resp.Status, nil
}

View File

@@ -1,41 +0,0 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grpc
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/viper"
"google.golang.org/grpc"
)
// Connect connects to an Ethereum 2 endpoint.
func Connect() (*grpc.ClientConn, error) {
connection := ""
if viper.GetString("connection") != "" {
connection = viper.GetString("connection")
}
if connection == "" {
return nil, errors.New("no connection")
}
// outputIf(debug, fmt.Sprintf("Connecting to %s", connection))
opts := []grpc.DialOption{grpc.WithInsecure()}
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
return grpc.DialContext(ctx, connection, opts...)
}

View File

@@ -1,101 +0,0 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grpc
import (
"context"
"time"
"github.com/gogo/protobuf/types"
"github.com/pkg/errors"
"github.com/spf13/viper"
"google.golang.org/grpc"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
)
// FetchGenesisTime fetches the genesis time.
func FetchGenesisTime(conn *grpc.ClientConn) (time.Time, error) {
if conn == nil {
return time.Now(), errors.New("no connection to beacon node")
}
client := ethpb.NewNodeClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
res, err := client.GetGenesis(ctx, &types.Empty{})
if err != nil {
return time.Now(), err
}
return time.Unix(res.GetGenesisTime().Seconds, 0), nil
}
// FetchGenesisValidatorsRoot fetches the genesis validators root.
func FetchGenesisValidatorsRoot(conn *grpc.ClientConn) ([]byte, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
client := ethpb.NewNodeClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
res, err := client.GetGenesis(ctx, &types.Empty{})
if err != nil {
return nil, err
}
return res.GetGenesisValidatorsRoot(), nil
}
// FetchDepositContractAddress fetches the address of the deposit contract.
func FetchDepositContractAddress(conn *grpc.ClientConn) ([]byte, error) {
if conn == nil {
return nil, errors.New("no connection to beacon node")
}
client := ethpb.NewNodeClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
res, err := client.GetGenesis(ctx, &types.Empty{})
if err != nil {
return nil, err
}
return res.DepositContractAddress, nil
}
// FetchVersion fetches the version and metadata from the server.
func FetchVersion(conn *grpc.ClientConn) (string, string, error) {
if conn == nil {
return "", "", errors.New("no connection to beacon node")
}
client := ethpb.NewNodeClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
version, err := client.GetVersion(ctx, &types.Empty{})
if err != nil {
return "", "", err
}
return version.Version, version.Metadata, nil
}
// FetchSyncing returns true if the node is syncing, otherwise false.
func FetchSyncing(conn *grpc.ClientConn) (bool, error) {
if conn == nil {
return false, errors.New("no connection to beacon node")
}
client := ethpb.NewNodeClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
syncStatus, err := client.GetSyncStatus(ctx, &types.Empty{})
if err != nil {
return false, err
}
return syncStatus.Syncing, nil
}

View File

@@ -17,13 +17,11 @@ import (
"context"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// unlock attempts to unlock an account. It returns true if the account was already unlocked.
func unlock(account e2wtypes.Account) (bool, error) {
// Unlock attempts to unlock an account. It returns true if the account was already unlocked.
func Unlock(ctx context.Context, account e2wtypes.Account, passphrases []string) (bool, error) {
locker, isAccountLocker := account.(e2wtypes.AccountLocker)
if !isAccountLocker {
// outputIf(debug, "Account does not support unlocking")
@@ -31,9 +29,7 @@ func unlock(account e2wtypes.Account) (bool, error) {
return true, nil
}
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
alreadyUnlocked, err := locker.IsUnlocked(ctx)
cancel()
if err != nil {
return false, errors.Wrap(err, "unable to ascertain if account is unlocked")
}
@@ -43,10 +39,8 @@ func unlock(account e2wtypes.Account) (bool, error) {
}
// Not already unlocked; attempt to unlock it.
for _, passphrase := range util.GetPassphrases() {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
for _, passphrase := range passphrases {
err = locker.Unlock(ctx, []byte(passphrase))
cancel()
if err == nil {
// Unlocked.
return false, nil
@@ -57,13 +51,11 @@ func unlock(account e2wtypes.Account) (bool, error) {
return false, errors.New("failed to unlock account")
}
// lock attempts to lock an account.
func lock(account e2wtypes.Account) error {
// Lock attempts to lock an account.
func Lock(ctx context.Context, account e2wtypes.Account) error {
locker, isAccountLocker := account.(e2wtypes.AccountLocker)
if !isAccountLocker {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
return locker.Lock(ctx)
}

View File

@@ -16,65 +16,59 @@ package signing
import (
"context"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// SignRoot signs a root with a domain.
func SignRoot(account e2wtypes.Account, root []byte, domain []byte) ([]byte, error) {
func SignRoot(ctx context.Context, account e2wtypes.Account, passphrases []string, root spec.Root, domain spec.Domain) (spec.BLSSignature, error) {
// Ensure input is as expected.
if account == nil {
return nil, errors.New("account not specified")
}
if len(root) != 32 {
return nil, errors.New("root must be 32 bytes in length")
}
if len(domain) != 32 {
return nil, errors.New("domain must be 32 bytes in length")
return spec.BLSSignature{}, errors.New("account not specified")
}
alreadyUnlocked, err := unlock(account)
alreadyUnlocked, err := Unlock(ctx, account, passphrases)
if err != nil {
return nil, err
return spec.BLSSignature{}, err
}
var signature e2types.Signature
// outputIf(debug, fmt.Sprintf("Signing %x (%d)", data, len(data)))
if protectingSigner, isProtectingSigner := account.(e2wtypes.AccountProtectingSigner); isProtectingSigner {
// Signer takes root and domain.
signature, err = signProtected(protectingSigner, root, domain)
signature, err = signProtected(ctx, protectingSigner, root, domain)
} else if signer, isSigner := account.(e2wtypes.AccountSigner); isSigner {
signature, err = sign(signer, root, domain)
signature, err = sign(ctx, signer, root, domain)
} else {
return nil, errors.New("account does not provide signing facility")
return spec.BLSSignature{}, errors.New("account does not provide signing facility")
}
if err != nil {
return nil, err
return spec.BLSSignature{}, err
}
if !alreadyUnlocked {
if err := lock(account); err != nil {
return nil, errors.Wrap(err, "failed to lock account")
if err := Lock(ctx, account); err != nil {
return spec.BLSSignature{}, errors.Wrap(err, "failed to lock account")
}
}
return signature.Marshal(), nil
var sig spec.BLSSignature
copy(sig[:], signature.Marshal())
return sig, nil
}
func sign(account e2wtypes.AccountSigner, root []byte, domain []byte) (e2types.Signature, error) {
func sign(ctx context.Context, account e2wtypes.AccountSigner, root spec.Root, domain spec.Domain) (e2types.Signature, error) {
container := &Container{
Root: root,
Domain: domain,
Root: root[:],
Domain: domain[:],
}
signingRoot, err := container.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to generate hash tree root")
}
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
signature, err := account.Sign(ctx, signingRoot[:])
if err != nil {
return nil, errors.Wrap(err, "failed to sign")
@@ -83,10 +77,8 @@ func sign(account e2wtypes.AccountSigner, root []byte, domain []byte) (e2types.S
return signature, err
}
func signProtected(account e2wtypes.AccountProtectingSigner, data []byte, domain []byte) (e2types.Signature, error) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
signature, err := account.SignGeneric(ctx, data, domain)
func signProtected(ctx context.Context, account e2wtypes.AccountProtectingSigner, root spec.Root, domain spec.Domain) (e2types.Signature, error) {
signature, err := account.SignGeneric(ctx, root[:], domain[:])
if err != nil {
return nil, errors.Wrap(err, "failed to sign")
}

View File

@@ -16,6 +16,8 @@ package testutil
import (
"encoding/hex"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
)
// HexToBytes converts a hex string to a byte array.
@@ -27,3 +29,57 @@ func HexToBytes(input string) []byte {
}
return res
}
// HexToPubKey converts a hex string to a spec public key.
// This should only be used for pre-defined test strings; it will panic if the input is invalid.
func HexToPubKey(input string) spec.BLSPubKey {
data := HexToBytes(input)
var res spec.BLSPubKey
copy(res[:], data)
return res
}
// HexToSignature converts a hex string to a spec signature.
// This should only be used for pre-defined test strings; it will panic if the input is invalid.
func HexToSignature(input string) spec.BLSSignature {
data := HexToBytes(input)
var res spec.BLSSignature
copy(res[:], data)
return res
}
// HexToDomainType converts a hex string to a spec domain type.
// This should only be used for pre-defined test strings; it will panic if the input is invalid.
func HexToDomainType(input string) spec.DomainType {
data := HexToBytes(input)
var res spec.DomainType
copy(res[:], data)
return res
}
// HexToDomain converts a hex string to a spec domain.
// This should only be used for pre-defined test strings; it will panic if the input is invalid.
func HexToDomain(input string) spec.Domain {
data := HexToBytes(input)
var res spec.Domain
copy(res[:], data)
return res
}
// HexToVersion converts a hex string to a spec version.
// This should only be used for pre-defined test strings; it will panic if the input is invalid.
func HexToVersion(input string) spec.Version {
data := HexToBytes(input)
var res spec.Version
copy(res[:], data)
return res
}
// HexToRoot converts a hex string to a spec root.
// This should only be used for pre-defined test strings; it will panic if the input is invalid.
func HexToRoot(input string) spec.Root {
data := HexToBytes(input)
var res spec.Root
copy(res[:], data)
return res
}

62
util/account.go Normal file
View File

@@ -0,0 +1,62 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"context"
"github.com/pkg/errors"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// UnlockAccount attempts to unlock an account. It returns true if the account was already unlocked.
func UnlockAccount(ctx context.Context, account e2wtypes.Account, passphrases []string) (bool, error) {
locker, isAccountLocker := account.(e2wtypes.AccountLocker)
if !isAccountLocker {
// This account doesn't support unlocking; return okay.
return true, nil
}
alreadyUnlocked, err := locker.IsUnlocked(ctx)
if err != nil {
return false, errors.Wrap(err, "unable to ascertain if account is unlocked")
}
if alreadyUnlocked {
return true, nil
}
// Not already unlocked; attempt to unlock it.
for _, passphrase := range passphrases {
err = locker.Unlock(ctx, []byte(passphrase))
if err == nil {
// Unlocked.
return false, nil
}
}
// Failed to unlock it.
return false, errors.New("failed to unlock account")
}
// LockAccount attempts to lock an account.
func LockAccount(ctx context.Context, account e2wtypes.Account) error {
locker, isAccountLocker := account.(e2wtypes.AccountLocker)
if !isAccountLocker {
// This account doesn't support locking; return okay.
return nil
}
return locker.Lock(ctx)
}

52
util/beaconnode.go Normal file
View File

@@ -0,0 +1,52 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"context"
"fmt"
"net/url"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/auto"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
// ConnectToBeaconNode connects to a beacon node at the given address.
func ConnectToBeaconNode(ctx context.Context, address string, timeout time.Duration, allowInsecure bool) (eth2client.Service, error) {
if !allowInsecure {
// Ensure the connection is either secure or local.
connectionURL, err := url.Parse(address)
if err != nil {
return nil, errors.Wrap(err, "failed to parse connection")
}
if connectionURL.Scheme == "http" &&
connectionURL.Host != "localhost" &&
connectionURL.Host != "127.0.0.1" {
fmt.Println("Connections to remote beacon nodes should be secure. This warning can be silenced with --allow-insecure-connections")
}
}
eth2Client, err := auto.New(ctx,
auto.WithLogLevel(zerolog.Disabled),
auto.WithAddress(address),
auto.WithTimeout(timeout),
)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to beacon node")
}
return eth2Client, nil
}

55
util/networks.go Normal file
View File

@@ -0,0 +1,55 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
)
// networks is a map of deposit contract addresses to networks.
var networks = map[string]string{
"00000000219ab540356cbb839cbe05303d7705fa": "Mainnet",
"07b39f4fde4a38bace212b546dac87c58dfe3fdc": "Medalla",
}
// Network returns the name of the network., calculated from the deposit contract information.
// If not known, returns "Unknown".
func Network(ctx context.Context, eth2Client eth2client.Service) (string, error) {
var address []byte
var err error
if provider, isProvider := eth2Client.(eth2client.DepositContractProvider); isProvider {
address, err = provider.DepositContractAddress(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain deposit contract address")
}
} else if provider, isProvider := eth2Client.(eth2client.SpecProvider); isProvider {
config, err := provider.Spec(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain deposit contract address")
}
address = config["DEPOSIT_CONTRACT_ADDRESS"].([]byte)
}
// outputIf(debug, fmt.Sprintf("Deposit contract is %#x", address))
depositContract := fmt.Sprintf("%x", address)
if network, exists := networks[depositContract]; exists {
return network, nil
}
return "Unknown", nil
}

View File

@@ -14,17 +14,18 @@
package util
import (
"context"
"errors"
"github.com/google/uuid"
types "github.com/wealdtech/go-eth2-types/v2"
e2types "github.com/wealdtech/go-eth2-types/v2"
)
// ScratchAccount is an account that exists temporarily.
type ScratchAccount struct {
id uuid.UUID
privKey types.PrivateKey
pubKey types.PublicKey
privKey e2types.PrivateKey
pubKey e2types.PublicKey
unlocked bool
}
@@ -37,7 +38,7 @@ func NewScratchAccount(privKey []byte, pubKey []byte) (*ScratchAccount, error) {
}
func newScratchAccountFromPrivKey(privKey []byte) (*ScratchAccount, error) {
key, err := types.BLSPrivateKeyFromBytes(privKey)
key, err := e2types.BLSPrivateKeyFromBytes(privKey)
if err != nil {
return nil, err
}
@@ -49,7 +50,7 @@ func newScratchAccountFromPrivKey(privKey []byte) (*ScratchAccount, error) {
}
func newScratchAccountFromPubKey(pubKey []byte) (*ScratchAccount, error) {
key, err := types.BLSPublicKeyFromBytes(pubKey)
key, err := e2types.BLSPublicKeyFromBytes(pubKey)
if err != nil {
return nil, err
}
@@ -67,7 +68,7 @@ func (a *ScratchAccount) Name() string {
return "scratch"
}
func (a *ScratchAccount) PublicKey() types.PublicKey {
func (a *ScratchAccount) PublicKey() e2types.PublicKey {
return a.pubKey
}
@@ -75,21 +76,22 @@ func (a *ScratchAccount) Path() string {
return ""
}
func (a *ScratchAccount) Lock() {
func (a *ScratchAccount) Lock(ctx context.Context) error {
a.unlocked = false
return nil
}
func (a *ScratchAccount) Unlock([]byte) error {
func (a *ScratchAccount) Unlock(ctx context.Context, passphrase []byte) error {
a.unlocked = true
return nil
}
func (a *ScratchAccount) IsUnlocked() bool {
return a.unlocked
func (a *ScratchAccount) IsUnlocked(ctx context.Context) (bool, error) {
return a.unlocked, nil
}
func (a *ScratchAccount) Sign(data []byte) (types.Signature, error) {
if !a.IsUnlocked() {
func (a *ScratchAccount) Sign(ctx context.Context, data []byte) (e2types.Signature, error) {
if !a.unlocked {
return nil, errors.New("locked")
}
if a.privKey == nil {

View File

@@ -14,6 +14,7 @@
package util_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
@@ -62,20 +63,27 @@ func TestScratchAccountFromPrivKey(t *testing.T) {
require.Equal(t, "scratch", account.Name())
require.Equal(t, "", account.Path())
require.NotNil(t, account.PublicKey())
require.False(t, account.IsUnlocked())
_, err := account.Sign(testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"))
unlocked, err := account.IsUnlocked(context.Background())
require.NoError(t, err)
require.False(t, unlocked)
_, err = account.Sign(context.Background(), testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"))
require.EqualError(t, err, "locked")
require.NoError(t, account.Unlock(nil))
require.True(t, account.IsUnlocked())
signature, err := account.Sign(testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"))
err = account.Unlock(context.Background(), nil)
require.NoError(t, err)
unlocked, err = account.IsUnlocked(context.Background())
require.NoError(t, err)
require.True(t, unlocked)
signature, err := account.Sign(context.Background(), testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"))
if test.sigErr == "" {
require.NoError(t, err)
require.Equal(t, test.signature, signature.Marshal())
} else {
require.EqualError(t, err, test.sigErr)
}
account.Lock()
require.False(t, account.IsUnlocked())
require.NoError(t, account.Lock(context.Background()))
unlocked, err = account.IsUnlocked(context.Background())
require.NoError(t, err)
require.False(t, unlocked)
} else {
require.EqualError(t, err, test.err)
}
@@ -119,15 +127,21 @@ func TestScratchAccountFromPublicKey(t *testing.T) {
require.NotNil(t, account.ID())
require.Equal(t, "scratch", account.Name())
require.Equal(t, "", account.Path())
require.False(t, account.IsUnlocked())
_, err := account.Sign(testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"))
unlocked, err := account.IsUnlocked(context.Background())
require.NoError(t, err)
require.False(t, unlocked)
_, err = account.Sign(context.Background(), testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"))
require.EqualError(t, err, "locked")
require.NoError(t, account.Unlock(nil))
require.True(t, account.IsUnlocked())
_, err = account.Sign(testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"))
require.NoError(t, account.Unlock(context.Background(), nil))
unlocked, err = account.IsUnlocked(context.Background())
require.NoError(t, err)
require.True(t, unlocked)
_, err = account.Sign(context.Background(), testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"))
require.EqualError(t, err, "no private key")
account.Lock()
require.False(t, account.IsUnlocked())
account.Lock(context.Background())
unlocked, err = account.IsUnlocked(context.Background())
require.NoError(t, err)
require.False(t, unlocked)
} else {
require.EqualError(t, err, test.err)
}

63
util/validatorexitdata.go Normal file
View File

@@ -0,0 +1,63 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"encoding/hex"
"encoding/json"
"fmt"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
// ValidatorExitData contains data for a validator exit.
type ValidatorExitData struct {
Data *spec.SignedVoluntaryExit
ForkVersion spec.Version
}
type validatorExitJSON struct {
Data *spec.SignedVoluntaryExit `json:"data"`
ForkVersion string `json:"fork_version"`
}
// MarshalJSON implements custom JSON marshaller.
func (d *ValidatorExitData) MarshalJSON() ([]byte, error) {
validatorExitJSON := &validatorExitJSON{
Data: d.Data,
ForkVersion: fmt.Sprintf("%#x", d.ForkVersion),
}
return json.Marshal(validatorExitJSON)
}
// UnmarshalJSON implements custom JSON unmarshaller.
func (d *ValidatorExitData) UnmarshalJSON(data []byte) error {
validatorExitJSON := &validatorExitJSON{}
if err := json.Unmarshal(data, validatorExitJSON); err != nil {
return errors.Wrap(err, "failed to unmarshal JSON")
}
d.Data = validatorExitJSON.Data
forkVersion, err := hex.DecodeString(strings.TrimPrefix(validatorExitJSON.ForkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "failed to parse fork version")
}
copy(d.ForkVersion[:], forkVersion)
return nil
}

View File

@@ -0,0 +1,57 @@
// Copyright © 2020 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/util"
)
func TestUnmarshal(t *testing.T) {
tests := []struct {
name string
in []byte
err string
}{
{
name: "Nil",
err: "unexpected end of JSON input",
},
{
name: "Invalid",
in: []byte(`invalid`),
err: "invalid character 'i' looking for beginning of value",
},
{
name: "Good",
in: []byte(`{"data":{"message":{"epoch":"0","validator_index":"0"},"signature":"0xb74eade64ebf1e02cc57e5d29517032c6ca99132fb8e7fb7e6d58c68713e581ef0ef88e2a6c599a007d997782abdd50b0f9763500a93a971c89cb2275583fe755d7c0e64f459ff22fcef5cab3f80848f0356e67c142b9cf3ee65613f56283d6e"},"fork_version":"0x00000001"}`),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var res util.ValidatorExitData
err := json.Unmarshal(test.in, &res)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}