Add validtor duties; update validator exit

This commit is contained in:
Jim McDonald
2020-12-09 20:33:30 +00:00
parent 688db9ef8c
commit 7aeba43338
21 changed files with 780 additions and 42 deletions

View File

@@ -1,3 +1,7 @@
Development:
- fix issue where base directory was ignored for wallet creation
- new "validator duties" command to display known duties for a given validator
- update go-eth2-client to display correct validator status from prysm
1.7.2:
- new "account derive" command to derive keys directly from a mnemonic and derivation path
- add more output to "deposit verify" to explain operation

View File

@@ -50,8 +50,8 @@ In quiet mode this will return 0 if the the exit is verified correctly, otherwis
account, err := exitVerifyAccount(ctx)
errCheck(err, "Failed to obtain account")
assert(viper.GetString("exit.data") != "", "exit data is required")
data, err := obtainExitData(viper.GetString("exit.Data"))
assert(viper.GetString("exit") != "", "exit is required")
data, err := obtainExitData(viper.GetString("exit"))
errCheck(err, "Failed to obtain exit data")
// Confirm signature is good.
@@ -65,12 +65,12 @@ In quiet mode this will return 0 if the the exit is verified correctly, otherwis
var exitDomain spec.Domain
copy(exitDomain[:], domain)
exit := &spec.VoluntaryExit{
Epoch: data.Data.Message.Epoch,
ValidatorIndex: data.Data.Message.ValidatorIndex,
Epoch: data.Exit.Message.Epoch,
ValidatorIndex: data.Exit.Message.ValidatorIndex,
}
exitRoot, err := exit.HashTreeRoot()
errCheck(err, "Failed to obtain exit hash tree root")
sig, err := e2types.BLSSignatureFromBytes(data.Data.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(data.Exit.Signature[:])
errCheck(err, "Invalid signature")
verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig)
errCheck(err, "Failed to verify voluntary exit")
@@ -134,12 +134,12 @@ func exitVerifyAccount(ctx context.Context) (e2wtypes.Account, error) {
func init() {
exitCmd.AddCommand(exitVerifyCmd)
exitFlags(exitVerifyCmd)
exitVerifyCmd.Flags().String("data", "", "JSON data, or path to JSON data")
exitVerifyCmd.Flags().String("exit", "", "JSON data, or path to JSON data")
exitVerifyCmd.Flags().StringVar(&exitVerifyPubKey, "pubkey", "", "Public key for which to verify exit")
}
func exitVerifyBindings() {
if err := viper.BindPFlag("data", exitVerifyCmd.Flags().Lookup("data")); err != nil {
if err := viper.BindPFlag("exit", exitVerifyCmd.Flags().Lookup("exit")); err != nil {
panic(err)
}
}

View File

@@ -76,6 +76,8 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
exitVerifyBindings()
case "validator/depositdata":
validatorDepositdataBindings()
case "validator/duties":
validatorDutiesBindings()
case "validator/exit":
validatorExitBindings()
case "validator/info":

View File

@@ -0,0 +1,71 @@
// 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 validatorduties
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
// Ethereum 2 connection.
eth2Client string
allowInsecure bool
// Operation.
account string
pubKey string
index string
}
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")
// Ethereum 2 connection.
data.eth2Client = viper.GetString("connection")
if data.eth2Client == "" {
return nil, errors.New("connection is required")
}
data.allowInsecure = viper.GetBool("allow-insecure-connections")
// Account.
data.account = viper.GetString("account")
// PubKey.
data.pubKey = viper.GetString("pubkey")
// ID.
data.index = viper.GetString("index")
if data.account == "" && data.pubKey == "" && data.index == "" {
return nil, errors.New("account, pubkey or index required")
}
return data, nil
}

View File

@@ -0,0 +1,100 @@
// 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 validatorduties
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{}{
"connection": "http://locahost:4000",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "timeout is required",
},
{
name: "AccountMissing",
vars: map[string]interface{}{
"timeout": "5s",
"connection": "http://locahost:4000",
},
err: "account, pubkey or index required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "connection is required",
},
}
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,112 @@
// 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 validatorduties
import (
"context"
"fmt"
"strings"
"time"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
genesisTime time.Time
slotDuration time.Duration
slotsPerEpoch uint64
thisEpochAttesterDuty *api.AttesterDuty
thisEpochProposerDuties []*api.ProposerDuty
nextEpochAttesterDuty *api.AttesterDuty
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.quiet {
return "", nil
}
builder := strings.Builder{}
now := time.Now()
builder.WriteString("Current time: ")
builder.WriteString(now.Format("15:04:05\n"))
if data.thisEpochAttesterDuty != nil {
thisEpochAttesterSlot := data.thisEpochAttesterDuty.Slot
thisSlotStart := data.genesisTime.Add(time.Duration(thisEpochAttesterSlot) * data.slotDuration)
thisSlotEnd := thisSlotStart.Add(data.slotDuration)
if thisSlotEnd.After(now) {
builder.WriteString("Upcoming attestation slot this epoch: ")
builder.WriteString(thisSlotStart.Format("15:04:05"))
builder.WriteString(" - ")
builder.WriteString(thisSlotEnd.Format("15:04:05 ("))
until := thisSlotStart.Sub(now)
if until > 0 {
builder.WriteString(fmt.Sprintf("%ds until start of slot)\n", int(until.Seconds())))
} else {
builder.WriteString("\n")
}
}
}
for _, proposerDuty := range data.thisEpochProposerDuties {
proposerSlot := proposerDuty.Slot
proposerSlotStart := data.genesisTime.Add(time.Duration(proposerSlot) * data.slotDuration)
proposerSlotEnd := proposerSlotStart.Add(data.slotDuration)
builder.WriteString("Upcoming proposer slot this epoch: ")
builder.WriteString(proposerSlotStart.Format("15:04:05"))
builder.WriteString(" - ")
builder.WriteString(proposerSlotEnd.Format("15:04:05 ("))
until := proposerSlotStart.Sub(now)
if until > 0 {
builder.WriteString(fmt.Sprintf("%ds until start of slot)\n", int(until.Seconds())))
} else {
builder.WriteString("\n")
}
}
if data.nextEpochAttesterDuty != nil {
nextEpochAttesterSlot := data.nextEpochAttesterDuty.Slot
nextSlotStart := data.genesisTime.Add(time.Duration(nextEpochAttesterSlot) * data.slotDuration)
nextSlotEnd := nextSlotStart.Add(data.slotDuration)
builder.WriteString("Upcoming attestation slot next epoch: ")
builder.WriteString(nextSlotStart.Format("15:04:05"))
builder.WriteString(" - ")
builder.WriteString(nextSlotEnd.Format("15:04:05 ("))
until := nextSlotStart.Sub(now)
builder.WriteString(fmt.Sprintf("%ds until start of slot)\n", int(until.Seconds())))
nextEpoch := uint64(data.nextEpochAttesterDuty.Slot) / data.slotsPerEpoch
nextEpochStart := data.genesisTime.Add(time.Duration(nextEpoch*data.slotsPerEpoch) * data.slotDuration)
builder.WriteString("Next epoch starts ")
builder.WriteString(nextEpochStart.Format("15:04:05 ("))
until = nextEpochStart.Sub(now)
if until > 0 {
builder.WriteString(fmt.Sprintf("%ds until start of epoch)\n", int(until.Seconds())))
} else {
builder.WriteString("\n")
}
}
return builder.String(), nil
}

View File

@@ -0,0 +1,83 @@
// 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 validatorduties
import (
"context"
"strings"
"testing"
"time"
api "github.com/attestantio/go-eth2-client/api/v1"
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
expected []string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Empty",
dataOut: &dataOut{},
expected: []string{"Current time"},
},
{
name: "Found",
dataOut: &dataOut{
genesisTime: time.Unix(16000000000, 0),
slotDuration: 12 * time.Second,
slotsPerEpoch: 32,
thisEpochAttesterDuty: &api.AttesterDuty{
Slot: spec.Slot(1),
},
thisEpochProposerDuties: []*api.ProposerDuty{
{
Slot: spec.Slot(2),
},
},
nextEpochAttesterDuty: &api.AttesterDuty{
Slot: spec.Slot(40),
},
},
expected: []string{
"Current time",
"Upcoming attestation slot this epoch",
"Upcoming proposer slot this epoch",
"Upcoming attestation slot next epoch",
},
},
}
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)
for _, expected := range test.expected {
require.True(t, strings.Contains(res, expected))
}
}
})
}
}

View File

@@ -0,0 +1,182 @@
// 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 validatorduties
import (
"context"
"encoding/hex"
"fmt"
"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"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
// Ethereum 2 client.
eth2Client, err := util.ConnectToBeaconNode(ctx, data.eth2Client, data.timeout, data.allowInsecure)
if err != nil {
return nil, err
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
validatorIndex, err := validatorIndex(ctx, eth2Client, data)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator index")
}
// Fetch duties for this and next epoch.
thisEpoch, err := currentEpoch(ctx, eth2Client)
if err != nil {
return nil, errors.Wrap(err, "failed to calculate current epoch")
}
thisEpochAttesterDuty, err := attesterDuty(ctx, eth2Client, validatorIndex, thisEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain this epoch duty for validator")
}
results.thisEpochAttesterDuty = thisEpochAttesterDuty
thisEpochProposerDuties, err := proposerDuties(ctx, eth2Client, validatorIndex, thisEpoch)
results.thisEpochProposerDuties = thisEpochProposerDuties
nextEpoch := thisEpoch + 1
nextEpochAttesterDuty, err := attesterDuty(ctx, eth2Client, validatorIndex, nextEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain next epoch duty for validator")
}
results.nextEpochAttesterDuty = nextEpochAttesterDuty
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis data")
}
results.genesisTime = genesis.GenesisTime
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
return results, nil
}
func attesterDuty(ctx context.Context, eth2Client eth2client.Service, validatorIndex spec.ValidatorIndex, epoch spec.Epoch) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validatorIndex})
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
}
func proposerDuties(ctx context.Context, eth2Client eth2client.Service, validatorIndex spec.ValidatorIndex, epoch spec.Epoch) ([]*api.ProposerDuty, error) {
// Fetch the proposer duties for this epoch.
proposerDuties, err := eth2Client.(eth2client.ProposerDutiesProvider).ProposerDuties(ctx, epoch, []spec.ValidatorIndex{validatorIndex})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain proposer duties")
}
return proposerDuties, nil
}
func currentEpoch(ctx context.Context, eth2Client eth2client.Service) (spec.Epoch, error) {
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return 0, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return 0, errors.Wrap(err, "failed to obtain genesis data")
}
if genesis.GenesisTime.After(time.Now()) {
return spec.Epoch(0), nil
}
return spec.Epoch(uint64(time.Since(genesis.GenesisTime).Seconds()) / (uint64(slotDuration.Seconds()) * slotsPerEpoch)), nil
}
// validatorIndex obtains the index of a validator
func validatorIndex(ctx context.Context, eth2Client eth2client.Service, data *dataIn) (spec.ValidatorIndex, error) {
switch {
case data.account != "":
ctx, cancel := context.WithTimeout(context.Background(), data.timeout)
defer cancel()
_, account, err := util.WalletAndAccountFromPath(ctx, data.account)
if err != nil {
return 0, errors.Wrap(err, "failed to obtain account")
}
return accountToIndex(ctx, account, eth2Client)
case data.pubKey != "":
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
if err != nil {
return 0, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
}
account, err := util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return 0, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
}
return accountToIndex(ctx, account, eth2Client)
case data.index != "":
val, err := strconv.ParseUint(data.index, 10, 64)
if err != nil {
return 0, err
}
return spec.ValidatorIndex(val), nil
default:
return 0, errors.New("no validator")
}
}
func accountToIndex(ctx context.Context, account e2wtypes.Account, eth2Client eth2client.Service) (spec.ValidatorIndex, error) {
pubKey, err := util.BestPublicKey(account)
if err != nil {
return 0, err
}
pubKeys := make([]spec.BLSPubKey, 1)
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, "head", pubKeys)
if err != nil {
return 0, err
}
for index := range validators {
return index, nil
}
return 0, errors.New("validator not found")
}

View File

@@ -0,0 +1,60 @@
// 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 validatorduties
import (
"context"
"os"
"testing"
"time"
"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")
}
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Client",
dataIn: &dataIn{
timeout: 5 * time.Second,
eth2Client: os.Getenv("ETHDO_TEST_CONNECTION"),
allowInsecure: true,
index: "1",
},
},
}
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 validatorduties
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

@@ -79,7 +79,7 @@ func inputJSON(ctx context.Context, data *dataIn) (*dataIn, error) {
if err != nil {
return nil, err
}
data.signedVoluntaryExit = validatorData.Data
data.signedVoluntaryExit = validatorData.Exit
return inputChainData(ctx, data)
}

View File

@@ -91,9 +91,10 @@ func TestInput(t *testing.T) {
{
name: "KeyGood",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
},
res: &dataIn{
timeout: 5 * time.Second,
@@ -102,9 +103,10 @@ func TestInput(t *testing.T) {
{
name: "AccountUnknown",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"account": "Test wallet/unknown",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"account": "Test wallet/unknown",
},
res: &dataIn{
timeout: 5 * time.Second,
@@ -114,9 +116,10 @@ func TestInput(t *testing.T) {
{
name: "AccountGood",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"account": "Test wallet/Interop 0",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"account": "Test wallet/Interop 0",
},
res: &dataIn{
timeout: 5 * time.Second,
@@ -125,9 +128,10 @@ func TestInput(t *testing.T) {
{
name: "JSONInvalid",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"exit": `invalid`,
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"exit": `invalid`,
},
res: &dataIn{
timeout: 5 * time.Second,
@@ -137,9 +141,10 @@ func TestInput(t *testing.T) {
{
name: "JSONGood",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"timeout": "5s",
"exit": `{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"}`,
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"exit": `{"exit":{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"},"fork_version":"0x00002009"}`,
},
res: &dataIn{
timeout: 5 * time.Second,
@@ -148,19 +153,21 @@ func TestInput(t *testing.T) {
{
name: "ClientBad",
vars: map[string]interface{}{
"connection": "localhost:1",
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
"connection": "localhost:1",
"allow-insecure-connections": true,
"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",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
"epoch": "123",
},
res: &dataIn{
timeout: 5 * time.Second,

View File

@@ -46,7 +46,7 @@ func output(ctx context.Context, data *dataOut) (string, error) {
func outputJSON(ctx context.Context, data *dataOut) (string, error) {
validatorExitData := &util.ValidatorExitData{
Data: data.signedVoluntaryExit,
Exit: data.signedVoluntaryExit,
ForkVersion: data.forkVersion,
}
bytes, err := json.Marshal(validatorExitData)

View File

@@ -79,7 +79,7 @@ func TestOutput(t *testing.T) {
},
},
},
res: `{"data":{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"},"fork_version":"0x01020304"}`,
res: `{"exit":{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"},"fork_version":"0x01020304"}`,
},
}

61
cmd/validatorduties.go Normal file
View File

@@ -0,0 +1,61 @@
// 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/spf13/cobra"
"github.com/spf13/viper"
validatorduties "github.com/wealdtech/ethdo/cmd/validator/duties"
)
var validatorDutiesCmd = &cobra.Command{
Use: "duties",
Short: "List known duties for a validator",
Long: `List known duties for a validator. For example:
ethdo validator duties --account=Validators/One
Attester duties are known for the current and next epoch. Proposer duties are known for the current epoch.
In quiet mode this will return 0 if the the duties have been obtained, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorduties.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
fmt.Printf(res)
return nil
},
}
func init() {
validatorCmd.AddCommand(validatorDutiesCmd)
validatorFlags(validatorDutiesCmd)
validatorDutiesCmd.Flags().String("pubkey", "", "validator public key for duties")
validatorDutiesCmd.Flags().String("index", "", "validator index for duties")
}
func validatorDutiesBindings() {
if err := viper.BindPFlag("pubkey", validatorDutiesCmd.Flags().Lookup("pubkey")); err != nil {
panic(err)
}
if err := viper.BindPFlag("index", validatorDutiesCmd.Flags().Lookup("index")); err != nil {
panic(err)
}
}

View File

@@ -348,12 +348,12 @@ Exit commands focus on information about validator exits generated by the `ethdo
#### `verify`
`ethdo exit verify` verifies the validator exit information in a JSON file generated by the `ethdo validator exit` command. Options include:
- `data`: either a path to the JSON file or the JSON itself
- `exit`: either a path to the JSON file or the JSON itself
- `account`: the account that generated the exit transaction (if available as an account, in format "wallet/account")
- `pubkey`: the public key of the account that generated the exit transaction
```sh
$ ethdo exit verify --data=${HOME}/exit.json --pubkey=0xa951530887ae2494a8cc4f11cf186963b0051ac4f7942375585b9cf98324db1e532a67e521d0fcaab510edad1352394c
$ ethdo exit verify --exit=${HOME}/exit.json --pubkey=0xa951530887ae2494a8cc4f11cf186963b0051ac4f7942375585b9cf98324db1e532a67e521d0fcaab510edad1352394c
```
### `node` commands

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.3
github.com/attestantio/go-eth2-client v0.6.15
github.com/attestantio/go-eth2-client v0.6.16
github.com/aws/aws-sdk-go v1.36.2 // indirect
github.com/ferranbt/fastssz v0.0.0-20201207112544-98a5de30d648
github.com/fsnotify/fsnotify v1.4.9 // indirect

2
go.sum
View File

@@ -77,6 +77,8 @@ github.com/attestantio/go-eth2-client v0.6.10 h1:PMNBMLk6xfMEUqhaUnsI0/HZRrstZF1
github.com/attestantio/go-eth2-client v0.6.10/go.mod h1:ODAZ4yS1YYYew/EsgGsVb/siNEoa505CrGsvlVFdkfo=
github.com/attestantio/go-eth2-client v0.6.15 h1:GNkiSF2Dqp6qahMXMW8r8Wy61WEvytnAM+rEyutdfv8=
github.com/attestantio/go-eth2-client v0.6.15/go.mod h1:Hya4fp1ZLWAFI64qMhNbQgfY4StWiHulW4CFwu+vP3s=
github.com/attestantio/go-eth2-client v0.6.16 h1:2Xn5RKqXUXfxLYVHn3D6l0FK7NUCjzl5v4oYIxcxc5k=
github.com/attestantio/go-eth2-client v0.6.16/go.mod h1:Hya4fp1ZLWAFI64qMhNbQgfY4StWiHulW4CFwu+vP3s=
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=

View File

@@ -27,6 +27,10 @@ import (
// 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 timeout == 0 {
return nil, errors.New("no timeout specified")
}
if !allowInsecure {
// Ensure the connection is either secure or local.
connectionURL, err := url.Parse(address)

View File

@@ -56,7 +56,7 @@ func SetupStore() error {
opts = append(opts, filesystem.WithPassphrase([]byte(GetStorePassphrase())))
}
if GetBaseDir() != "" {
opts = append(opts, filesystem.WithLocation(viper.GetString("base-dir")))
opts = append(opts, filesystem.WithLocation(GetBaseDir()))
}
store = filesystem.New(opts...)
default:

View File

@@ -25,19 +25,19 @@ import (
// ValidatorExitData contains data for a validator exit.
type ValidatorExitData struct {
Data *spec.SignedVoluntaryExit
Exit *spec.SignedVoluntaryExit
ForkVersion spec.Version
}
type validatorExitJSON struct {
Data *spec.SignedVoluntaryExit `json:"data"`
Exit *spec.SignedVoluntaryExit `json:"exit"`
ForkVersion string `json:"fork_version"`
}
// MarshalJSON implements custom JSON marshaller.
func (d *ValidatorExitData) MarshalJSON() ([]byte, error) {
validatorExitJSON := &validatorExitJSON{
Data: d.Data,
Exit: d.Exit,
ForkVersion: fmt.Sprintf("%#x", d.ForkVersion),
}
return json.Marshal(validatorExitJSON)
@@ -51,10 +51,10 @@ func (d *ValidatorExitData) UnmarshalJSON(data []byte) error {
return errors.Wrap(err, "failed to unmarshal JSON")
}
if validatorExitJSON.Data == nil {
return errors.New("data missing")
if validatorExitJSON.Exit == nil {
return errors.New("exit missing")
}
d.Data = validatorExitJSON.Data
d.Exit = validatorExitJSON.Exit
if validatorExitJSON.ForkVersion == "" {
return errors.New("fork version missing")