mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-07 21:24:01 -05:00
Add validtor duties; update validator exit
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
71
cmd/validator/duties/input.go
Normal file
71
cmd/validator/duties/input.go
Normal 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
|
||||
}
|
||||
100
cmd/validator/duties/input_internal_test.go
Normal file
100
cmd/validator/duties/input_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
112
cmd/validator/duties/output.go
Normal file
112
cmd/validator/duties/output.go
Normal 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
|
||||
}
|
||||
83
cmd/validator/duties/output_internal_test.go
Normal file
83
cmd/validator/duties/output_internal_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
182
cmd/validator/duties/process.go
Normal file
182
cmd/validator/duties/process.go
Normal 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")
|
||||
}
|
||||
60
cmd/validator/duties/process_internal_test.go
Normal file
60
cmd/validator/duties/process_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/validator/duties/run.go
Normal file
50
cmd/validator/duties/run.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
61
cmd/validatorduties.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
2
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user