mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-09 22:18: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:
|
1.7.2:
|
||||||
- new "account derive" command to derive keys directly from a mnemonic and derivation path
|
- new "account derive" command to derive keys directly from a mnemonic and derivation path
|
||||||
- add more output to "deposit verify" to explain operation
|
- 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)
|
account, err := exitVerifyAccount(ctx)
|
||||||
errCheck(err, "Failed to obtain account")
|
errCheck(err, "Failed to obtain account")
|
||||||
|
|
||||||
assert(viper.GetString("exit.data") != "", "exit data is required")
|
assert(viper.GetString("exit") != "", "exit is required")
|
||||||
data, err := obtainExitData(viper.GetString("exit.Data"))
|
data, err := obtainExitData(viper.GetString("exit"))
|
||||||
errCheck(err, "Failed to obtain exit data")
|
errCheck(err, "Failed to obtain exit data")
|
||||||
|
|
||||||
// Confirm signature is good.
|
// 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
|
var exitDomain spec.Domain
|
||||||
copy(exitDomain[:], domain)
|
copy(exitDomain[:], domain)
|
||||||
exit := &spec.VoluntaryExit{
|
exit := &spec.VoluntaryExit{
|
||||||
Epoch: data.Data.Message.Epoch,
|
Epoch: data.Exit.Message.Epoch,
|
||||||
ValidatorIndex: data.Data.Message.ValidatorIndex,
|
ValidatorIndex: data.Exit.Message.ValidatorIndex,
|
||||||
}
|
}
|
||||||
exitRoot, err := exit.HashTreeRoot()
|
exitRoot, err := exit.HashTreeRoot()
|
||||||
errCheck(err, "Failed to obtain exit hash tree root")
|
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")
|
errCheck(err, "Invalid signature")
|
||||||
verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig)
|
verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig)
|
||||||
errCheck(err, "Failed to verify voluntary exit")
|
errCheck(err, "Failed to verify voluntary exit")
|
||||||
@@ -134,12 +134,12 @@ func exitVerifyAccount(ctx context.Context) (e2wtypes.Account, error) {
|
|||||||
func init() {
|
func init() {
|
||||||
exitCmd.AddCommand(exitVerifyCmd)
|
exitCmd.AddCommand(exitVerifyCmd)
|
||||||
exitFlags(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")
|
exitVerifyCmd.Flags().StringVar(&exitVerifyPubKey, "pubkey", "", "Public key for which to verify exit")
|
||||||
}
|
}
|
||||||
|
|
||||||
func exitVerifyBindings() {
|
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)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
|
|||||||
exitVerifyBindings()
|
exitVerifyBindings()
|
||||||
case "validator/depositdata":
|
case "validator/depositdata":
|
||||||
validatorDepositdataBindings()
|
validatorDepositdataBindings()
|
||||||
|
case "validator/duties":
|
||||||
|
validatorDutiesBindings()
|
||||||
case "validator/exit":
|
case "validator/exit":
|
||||||
validatorExitBindings()
|
validatorExitBindings()
|
||||||
case "validator/info":
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
data.signedVoluntaryExit = validatorData.Data
|
data.signedVoluntaryExit = validatorData.Exit
|
||||||
return inputChainData(ctx, data)
|
return inputChainData(ctx, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ func TestInput(t *testing.T) {
|
|||||||
name: "KeyGood",
|
name: "KeyGood",
|
||||||
vars: map[string]interface{}{
|
vars: map[string]interface{}{
|
||||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||||
|
"allow-insecure-connections": true,
|
||||||
"timeout": "5s",
|
"timeout": "5s",
|
||||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||||
},
|
},
|
||||||
@@ -103,6 +104,7 @@ func TestInput(t *testing.T) {
|
|||||||
name: "AccountUnknown",
|
name: "AccountUnknown",
|
||||||
vars: map[string]interface{}{
|
vars: map[string]interface{}{
|
||||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||||
|
"allow-insecure-connections": true,
|
||||||
"timeout": "5s",
|
"timeout": "5s",
|
||||||
"account": "Test wallet/unknown",
|
"account": "Test wallet/unknown",
|
||||||
},
|
},
|
||||||
@@ -115,6 +117,7 @@ func TestInput(t *testing.T) {
|
|||||||
name: "AccountGood",
|
name: "AccountGood",
|
||||||
vars: map[string]interface{}{
|
vars: map[string]interface{}{
|
||||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||||
|
"allow-insecure-connections": true,
|
||||||
"timeout": "5s",
|
"timeout": "5s",
|
||||||
"account": "Test wallet/Interop 0",
|
"account": "Test wallet/Interop 0",
|
||||||
},
|
},
|
||||||
@@ -126,6 +129,7 @@ func TestInput(t *testing.T) {
|
|||||||
name: "JSONInvalid",
|
name: "JSONInvalid",
|
||||||
vars: map[string]interface{}{
|
vars: map[string]interface{}{
|
||||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||||
|
"allow-insecure-connections": true,
|
||||||
"timeout": "5s",
|
"timeout": "5s",
|
||||||
"exit": `invalid`,
|
"exit": `invalid`,
|
||||||
},
|
},
|
||||||
@@ -138,8 +142,9 @@ func TestInput(t *testing.T) {
|
|||||||
name: "JSONGood",
|
name: "JSONGood",
|
||||||
vars: map[string]interface{}{
|
vars: map[string]interface{}{
|
||||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||||
|
"allow-insecure-connections": true,
|
||||||
"timeout": "5s",
|
"timeout": "5s",
|
||||||
"exit": `{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"}`,
|
"exit": `{"exit":{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"},"fork_version":"0x00002009"}`,
|
||||||
},
|
},
|
||||||
res: &dataIn{
|
res: &dataIn{
|
||||||
timeout: 5 * time.Second,
|
timeout: 5 * time.Second,
|
||||||
@@ -149,6 +154,7 @@ func TestInput(t *testing.T) {
|
|||||||
name: "ClientBad",
|
name: "ClientBad",
|
||||||
vars: map[string]interface{}{
|
vars: map[string]interface{}{
|
||||||
"connection": "localhost:1",
|
"connection": "localhost:1",
|
||||||
|
"allow-insecure-connections": true,
|
||||||
"timeout": "5s",
|
"timeout": "5s",
|
||||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||||
},
|
},
|
||||||
@@ -158,6 +164,7 @@ func TestInput(t *testing.T) {
|
|||||||
name: "EpochProvided",
|
name: "EpochProvided",
|
||||||
vars: map[string]interface{}{
|
vars: map[string]interface{}{
|
||||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||||
|
"allow-insecure-connections": true,
|
||||||
"timeout": "5s",
|
"timeout": "5s",
|
||||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||||
"epoch": "123",
|
"epoch": "123",
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func output(ctx context.Context, data *dataOut) (string, error) {
|
|||||||
|
|
||||||
func outputJSON(ctx context.Context, data *dataOut) (string, error) {
|
func outputJSON(ctx context.Context, data *dataOut) (string, error) {
|
||||||
validatorExitData := &util.ValidatorExitData{
|
validatorExitData := &util.ValidatorExitData{
|
||||||
Data: data.signedVoluntaryExit,
|
Exit: data.signedVoluntaryExit,
|
||||||
ForkVersion: data.forkVersion,
|
ForkVersion: data.forkVersion,
|
||||||
}
|
}
|
||||||
bytes, err := json.Marshal(validatorExitData)
|
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`
|
#### `verify`
|
||||||
|
|
||||||
`ethdo exit verify` verifies the validator exit information in a JSON file generated by the `ethdo validator exit` command. Options include:
|
`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")
|
- `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
|
- `pubkey`: the public key of the account that generated the exit transaction
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ ethdo exit verify --data=${HOME}/exit.json --pubkey=0xa951530887ae2494a8cc4f11cf186963b0051ac4f7942375585b9cf98324db1e532a67e521d0fcaab510edad1352394c
|
$ ethdo exit verify --exit=${HOME}/exit.json --pubkey=0xa951530887ae2494a8cc4f11cf186963b0051ac4f7942375585b9cf98324db1e532a67e521d0fcaab510edad1352394c
|
||||||
```
|
```
|
||||||
|
|
||||||
### `node` commands
|
### `node` commands
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.13
|
|||||||
require (
|
require (
|
||||||
github.com/OneOfOne/xxhash v1.2.5 // indirect
|
github.com/OneOfOne/xxhash v1.2.5 // indirect
|
||||||
github.com/attestantio/dirk v0.9.3
|
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/aws/aws-sdk-go v1.36.2 // indirect
|
||||||
github.com/ferranbt/fastssz v0.0.0-20201207112544-98a5de30d648
|
github.com/ferranbt/fastssz v0.0.0-20201207112544-98a5de30d648
|
||||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
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.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 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.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-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.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
github.com/aws/aws-sdk-go v1.32.6 h1:HoswAabUWgnrUF7X/9dr4WRgrr8DyscxXvTDm7Qw/5c=
|
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.
|
// 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) {
|
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 {
|
if !allowInsecure {
|
||||||
// Ensure the connection is either secure or local.
|
// Ensure the connection is either secure or local.
|
||||||
connectionURL, err := url.Parse(address)
|
connectionURL, err := url.Parse(address)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func SetupStore() error {
|
|||||||
opts = append(opts, filesystem.WithPassphrase([]byte(GetStorePassphrase())))
|
opts = append(opts, filesystem.WithPassphrase([]byte(GetStorePassphrase())))
|
||||||
}
|
}
|
||||||
if GetBaseDir() != "" {
|
if GetBaseDir() != "" {
|
||||||
opts = append(opts, filesystem.WithLocation(viper.GetString("base-dir")))
|
opts = append(opts, filesystem.WithLocation(GetBaseDir()))
|
||||||
}
|
}
|
||||||
store = filesystem.New(opts...)
|
store = filesystem.New(opts...)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -25,19 +25,19 @@ import (
|
|||||||
|
|
||||||
// ValidatorExitData contains data for a validator exit.
|
// ValidatorExitData contains data for a validator exit.
|
||||||
type ValidatorExitData struct {
|
type ValidatorExitData struct {
|
||||||
Data *spec.SignedVoluntaryExit
|
Exit *spec.SignedVoluntaryExit
|
||||||
ForkVersion spec.Version
|
ForkVersion spec.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
type validatorExitJSON struct {
|
type validatorExitJSON struct {
|
||||||
Data *spec.SignedVoluntaryExit `json:"data"`
|
Exit *spec.SignedVoluntaryExit `json:"exit"`
|
||||||
ForkVersion string `json:"fork_version"`
|
ForkVersion string `json:"fork_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements custom JSON marshaller.
|
// MarshalJSON implements custom JSON marshaller.
|
||||||
func (d *ValidatorExitData) MarshalJSON() ([]byte, error) {
|
func (d *ValidatorExitData) MarshalJSON() ([]byte, error) {
|
||||||
validatorExitJSON := &validatorExitJSON{
|
validatorExitJSON := &validatorExitJSON{
|
||||||
Data: d.Data,
|
Exit: d.Exit,
|
||||||
ForkVersion: fmt.Sprintf("%#x", d.ForkVersion),
|
ForkVersion: fmt.Sprintf("%#x", d.ForkVersion),
|
||||||
}
|
}
|
||||||
return json.Marshal(validatorExitJSON)
|
return json.Marshal(validatorExitJSON)
|
||||||
@@ -51,10 +51,10 @@ func (d *ValidatorExitData) UnmarshalJSON(data []byte) error {
|
|||||||
return errors.Wrap(err, "failed to unmarshal JSON")
|
return errors.Wrap(err, "failed to unmarshal JSON")
|
||||||
}
|
}
|
||||||
|
|
||||||
if validatorExitJSON.Data == nil {
|
if validatorExitJSON.Exit == nil {
|
||||||
return errors.New("data missing")
|
return errors.New("exit missing")
|
||||||
}
|
}
|
||||||
d.Data = validatorExitJSON.Data
|
d.Exit = validatorExitJSON.Exit
|
||||||
|
|
||||||
if validatorExitJSON.ForkVersion == "" {
|
if validatorExitJSON.ForkVersion == "" {
|
||||||
return errors.New("fork version missing")
|
return errors.New("fork version missing")
|
||||||
|
|||||||
Reference in New Issue
Block a user