Compare commits

..

3 Commits

Author SHA1 Message Date
Jim McDonald
7596d271ad Linting. 2021-02-10 10:24:49 +00:00
Jim McDonald
943f9350f3 Add 'chain time' and 'validator keycheck' commands. 2021-02-10 10:13:24 +00:00
Jim McDonald
07863846e6 Use double quotes for Windows compatability. 2021-02-04 21:54:43 +00:00
30 changed files with 1208 additions and 19 deletions

23
.github/workflows/golangci-lint.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: golangci-lint
on: [ push, pull_request ]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.29
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
args: --timeout=10m
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

View File

@@ -1,3 +1,7 @@
1.8.0
- add "chain time"
- add "validator keycheck"
1.7.5:
- add "slot time"
- add "attester duties"

View File

@@ -25,6 +25,9 @@ import (
func blsPrivateKey(input string) *e2types.BLSPrivateKey {
data, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
if err != nil {
panic(err)
}
key, err := e2types.BLSPrivateKeyFromBytes(data)
if err != nil {
panic(err)
@@ -58,7 +61,7 @@ func TestOutput(t *testing.T) {
{
name: "PrivatKey",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showPrivateKey: true,
},
needs: []string{"Public key", "Private key"},
@@ -75,7 +78,7 @@ func TestOutput(t *testing.T) {
name: "All",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showPrivateKey: true,
showPrivateKey: true,
showWithdrawalCredentials: true,
},
needs: []string{"Public key", "Private key", "Withdrawal credentials"},

View File

@@ -27,6 +27,3 @@ var attestationCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(attestationCmd)
}
func attestationFlags(cmd *cobra.Command) {
}

81
cmd/chain/time/input.go Normal file
View File

@@ -0,0 +1,81 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaintime
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
json bool
// Input
connection string
allowInsecureConnections bool
timestamp string
slot string
epoch 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")
data.json = viper.GetBool("json")
haveInput := false
if viper.GetString("timestamp") != "" {
data.timestamp = viper.GetString("timestamp")
haveInput = true
}
if viper.GetString("slot") != "" {
if haveInput {
return nil, errors.New("only one of timestamp, slot and epoch allowed")
}
data.slot = viper.GetString("slot")
haveInput = true
}
if viper.GetString("epoch") != "" {
if haveInput {
return nil, errors.New("only one of timestamp, slot and epoch allowed")
}
data.epoch = viper.GetString("epoch")
haveInput = true
}
if !haveInput {
return nil, errors.New("one of timestamp, slot or epoch required")
}
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
data.connection = viper.GetString("connection")
data.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return data, nil
}

View File

@@ -0,0 +1,97 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaintime
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"slot": "1",
},
err: "connection is required",
},
{
name: "IDMissing",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "one of timestamp, slot or epoch 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)
}
})
}
}

66
cmd/chain/time/output.go Normal file
View File

@@ -0,0 +1,66 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaintime
import (
"context"
"fmt"
"strings"
"time"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
epoch spec.Epoch
epochStart time.Time
epochEnd time.Time
slot spec.Slot
slotStart time.Time
slotEnd time.Time
}
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{}
builder.WriteString("Epoch ")
builder.WriteString(fmt.Sprintf("%d", data.epoch))
builder.WriteString("\n Epoch start ")
builder.WriteString(data.epochStart.Format("2006-01-02 15:04:05"))
builder.WriteString("\n Epoch end ")
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
builder.WriteString("\nSlot ")
builder.WriteString(fmt.Sprintf("%d", data.slot))
builder.WriteString("\n Slot start ")
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
builder.WriteString("\n Slot end ")
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
builder.WriteString("\n")
return builder.String(), nil
}

View File

@@ -0,0 +1,85 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaintime
// import (
// "context"
// "testing"
//
// api "github.com/attestantio/go-eth2-client/api/v1"
// "github.com/stretchr/testify/require"
// "github.com/wealdtech/ethdo/testutil"
// )
//
// func TestOutput(t *testing.T) {
// tests := []struct {
// name string
// dataOut *dataOut
// res string
// err string
// }{
// {
// name: "Nil",
// err: "no data",
// },
// {
// name: "Empty",
// dataOut: &dataOut{},
// res: "No duties found",
// },
// {
// name: "Present",
// dataOut: &dataOut{
// duty: &api.AttesterDuty{
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
// Slot: 1,
// ValidatorIndex: 2,
// CommitteeIndex: 3,
// CommitteeLength: 4,
// CommitteesAtSlot: 5,
// ValidatorCommitteeIndex: 6,
// },
// },
// res: "Validator attesting in slot 1 committee 3",
// },
// {
// name: "JSON",
// dataOut: &dataOut{
// json: true,
// duty: &api.AttesterDuty{
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
// Slot: 1,
// ValidatorIndex: 2,
// CommitteeIndex: 3,
// CommitteeLength: 4,
// CommitteesAtSlot: 5,
// ValidatorCommitteeIndex: 6,
// },
// },
// res: `{"pubkey":"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95","slot":"1","validator_index":"2","committee_index":"3","committee_length":"4","committees_at_slot":"5","validator_committee_index":"6"}`,
// },
// }
//
// for _, test := range tests {
// t.Run(test.name, func(t *testing.T) {
// res, err := output(context.Background(), test.dataOut)
// if test.err != "" {
// require.EqualError(t, err, test.err)
// } else {
// require.NoError(t, err)
// require.Equal(t, test.res, res)
// }
// })
// }
// }

89
cmd/chain/time/process.go Normal file
View File

@@ -0,0 +1,89 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaintime
import (
"context"
"strconv"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
eth2Client, err := util.ConnectToBeaconNode(ctx, data.connection, data.timeout, data.allowInsecureConnections)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, 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 nil, errors.Wrap(err, "failed to obtain genesis data")
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
// Calculate the slot given the input.
switch {
case data.slot != "":
slot, err := strconv.ParseUint(data.slot, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse slot")
}
results.slot = spec.Slot(slot)
case data.epoch != "":
epoch, err := strconv.ParseUint(data.epoch, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse epoch")
}
results.slot = spec.Slot(epoch * slotsPerEpoch)
case data.timestamp != "":
timestamp, err := time.Parse("2006-01-02T15:04:05", data.timestamp)
if err != nil {
return nil, errors.Wrap(err, "failed to parse timestamp")
}
secs := timestamp.Sub(genesis.GenesisTime)
if secs < 0 {
return nil, errors.New("timestamp prior to genesis")
}
results.slot = spec.Slot(secs / slotDuration)
}
// Fill in the info given the slot.
results.slotStart = genesis.GenesisTime.Add(time.Duration(results.slot) * slotDuration)
results.slotEnd = genesis.GenesisTime.Add(time.Duration(results.slot+1) * slotDuration)
results.epoch = spec.Epoch(uint64(results.slot) / slotsPerEpoch)
results.epochStart = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch)*slotsPerEpoch) * slotDuration)
results.epochEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch+1)*slotsPerEpoch) * slotDuration)
return results, nil
}

View File

@@ -0,0 +1,103 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaintime
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
expected *dataOut
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Slot",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
slot: "1",
},
expected: &dataOut{
epochStart: time.Unix(1606824023, 0),
epochEnd: time.Unix(1606824407, 0),
slot: 1,
slotStart: time.Unix(1606824035, 0),
slotEnd: time.Unix(1606824047, 0),
},
},
{
name: "Epoch",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
epoch: "2",
},
expected: &dataOut{
epoch: 2,
epochStart: time.Unix(1606824791, 0),
epochEnd: time.Unix(1606825175, 0),
slot: 64,
slotStart: time.Unix(1606824791, 0),
slotEnd: time.Unix(1606824803, 0),
},
},
{
name: "Timestamp",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
timestamp: "2021-01-01T00:00:00",
},
expected: &dataOut{
epoch: 6862,
epochStart: time.Unix(1609459031, 0),
epochEnd: time.Unix(1609459415, 0),
slot: 219598,
slotStart: time.Unix(1609459199, 0),
slotEnd: time.Unix(1609459211, 0),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, res)
}
})
}
}

50
cmd/chain/time/run.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaintime
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
}

60
cmd/chaintime.go Normal file
View File

@@ -0,0 +1,60 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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"
chaintime "github.com/wealdtech/ethdo/cmd/chain/time"
)
var chainTimeCmd = &cobra.Command{
Use: "time",
Short: "Obtain info about the chain at a given time",
Long: `Obtain info about the chain at a given time. For example:
ethdo chain time --slot=12345`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chaintime.Run(cmd)
if err != nil {
return err
}
if res != "" {
fmt.Print(res)
}
return nil
},
}
func init() {
chainCmd.AddCommand(chainTimeCmd)
chainFlags(chainTimeCmd)
chainTimeCmd.Flags().String("slot", "", "The slot for which to obtain information")
chainTimeCmd.Flags().String("epoch", "", "The epoch for which to obtain information")
chainTimeCmd.Flags().String("timestamp", "", "The timestamp for which to obtain information (format YYYY-MM-DDTHH:MM:SS)")
}
func chainTimeBindings() {
if err := viper.BindPFlag("slot", chainTimeCmd.Flags().Lookup("slot")); err != nil {
panic(err)
}
if err := viper.BindPFlag("epoch", chainTimeCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("timestamp", chainTimeCmd.Flags().Lookup("timestamp")); err != nil {
panic(err)
}
}

View File

@@ -246,7 +246,7 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
if err != nil {
return false, errors.Wrap(err, "failed to decode fork version")
}
if bytes.Equal(deposit.ForkVersion, forkVersion[:]) {
if bytes.Equal(deposit.ForkVersion, forkVersion) {
outputIf(!quiet, "Fork version verified")
} else {
outputIf(!quiet, "Fork version incorrect")

View File

@@ -23,8 +23,6 @@ import (
"github.com/pkg/errors"
)
var jsonOutput bool
func process(ctx context.Context, data *dataIn) error {
if data == nil {
return errors.New("no data")

View File

@@ -27,6 +27,3 @@ var proposerCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(proposerCmd)
}
func proposerFlags(cmd *cobra.Command) {
}

View File

@@ -74,6 +74,8 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
attesterInclusionBindings()
case "block/info":
blockInfoBindings()
case "chain/time":
chainTimeBindings()
case "exit/verify":
exitVerifyBindings()
case "node/events":
@@ -88,6 +90,8 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
validatorExitBindings()
case "validator/info":
validatorInfoBindings()
case "validator/keycheck":
validatorKeycheckBindings()
case "wallet/create":
walletCreateBindings()
case "wallet/import":

View File

@@ -40,5 +40,5 @@ func output(ctx context.Context, data *dataOut) (string, error) {
if data.verbose {
return fmt.Sprintf("%s - %s", data.startTime, data.endTime), nil
}
return fmt.Sprintf("%s", data.startTime), nil
return data.startTime.String(), nil
}

View File

@@ -53,7 +53,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
results.startTime = genesis.GenesisTime.Add(time.Duration((time.Duration(slot*int64(slotDuration.Seconds())) * time.Second)))
results.startTime = genesis.GenesisTime.Add((time.Duration(slot*int64(slotDuration.Seconds())) * time.Second))
results.endTime = results.startTime.Add(slotDuration)
return results, nil

View File

@@ -58,17 +58,20 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
thisEpochAttesterDuty, err := attesterDuty(ctx, eth2Client, validatorIndex, thisEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain this epoch duty for validator")
return nil, errors.Wrap(err, "failed to obtain this epoch attester duty for validator")
}
results.thisEpochAttesterDuty = thisEpochAttesterDuty
thisEpochProposerDuties, err := proposerDuties(ctx, eth2Client, validatorIndex, thisEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain this epoch proposer duties for validator")
}
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")
return nil, errors.Wrap(err, "failed to obtain next epoch attester duty for validator")
}
results.nextEpochAttesterDuty = nextEpochAttesterDuty

View File

@@ -0,0 +1,55 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorkeycheck
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
quiet bool
verbose bool
debug bool
// Withdrawal credentials.
withdrawalCredentials string
// Operation.
mnemonic string
privKey string
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Withdrawal credentials.
data.withdrawalCredentials = viper.GetString("withdrawal-credentials")
if data.withdrawalCredentials == "" {
return nil, errors.New("withdrawal credentials are required")
}
data.mnemonic = viper.GetString("mnemonic")
data.privKey = viper.GetString("privkey")
if data.mnemonic == "" && data.privKey == "" {
return nil, errors.New("mnemonic or privkey is required")
}
return data, nil
}

View File

@@ -0,0 +1,71 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorkeycheck
import (
"context"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "WithdrawalCredentialsMissing",
vars: map[string]interface{}{},
err: "withdrawal credentials are required",
},
{
name: "MnemonicAndPrivateKeyMissing",
vars: map[string]interface{}{
"withdrawal-credentials": "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
},
err: "mnemonic or privkey is required",
},
{
name: "GoodWithMnemonic",
vars: map[string]interface{}{
"withdrawal-credentials": "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
},
res: &dataIn{
withdrawalCredentials: "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
},
},
}
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.withdrawalCredentials, res.withdrawalCredentials)
}
})
}
}

View File

@@ -0,0 +1,52 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorkeycheck
import (
"context"
"fmt"
"os"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
match bool
path string
}
func output(ctx context.Context, data *dataOut) (string, int, error) {
if data == nil {
return "", 1, errors.New("no data")
}
if data.quiet {
if !data.match {
os.Exit(1)
}
return "", 1, nil
}
if data.match {
if data.path == "" {
return "Withdrawal credentials confirmed", 0, nil
}
return fmt.Sprintf("Withdrawal credentials confirmed at path %s", data.path), 0, nil
}
return "Could not confirm withdrawal credentials with given information", 1, nil
}

View File

@@ -0,0 +1,81 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorkeycheck
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
exitCode int
expected []string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Not found",
dataOut: &dataOut{
match: false,
},
exitCode: 1,
expected: []string{
"Could not confirm withdrawal credentials with given information",
},
},
{
name: "Found",
dataOut: &dataOut{
match: true,
},
expected: []string{
"Withdrawal credentials confirmed",
},
},
{
name: "FoundWithPath",
dataOut: &dataOut{
match: true,
path: "m/12381/3600/10/0",
},
expected: []string{
"Withdrawal credentials confirmed at path m/12381/3600/10/0",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, exitCode, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.exitCode, exitCode)
for _, expected := range test.expected {
require.True(t, strings.Contains(res, expected))
}
}
})
}
}

View File

@@ -0,0 +1,123 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorkeycheck
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/tyler-smith/go-bip39"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
"golang.org/x/text/unicode/norm"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
validatorWithdrawalCredentials, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalCredentials, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to parse withdrawal credentials")
}
match := false
path := ""
if data.privKey != "" {
// Single private key to check.
keyBytes, err := hex.DecodeString(strings.TrimPrefix(data.privKey, "0x"))
if err != nil {
return nil, err
}
key, err := e2types.BLSPrivateKeyFromBytes(keyBytes)
if err != nil {
return nil, err
}
match, err = checkPrivKey(ctx, data.debug, validatorWithdrawalCredentials, key)
if err != nil {
return nil, err
}
} else {
// Mnemonic to check.
match, path, err = checkMnemonic(ctx, data.debug, validatorWithdrawalCredentials, data.mnemonic)
if err != nil {
return nil, err
}
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
match: match,
path: path,
}
return results, nil
}
func checkPrivKey(ctx context.Context, debug bool, validatorWithdrawalCredentials []byte, key *e2types.BLSPrivateKey) (bool, error) {
pubKey := key.PublicKey()
withdrawalCredentials := util.SHA256(pubKey.Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
return bytes.Equal(withdrawalCredentials, validatorWithdrawalCredentials), nil
}
func checkMnemonic(ctx context.Context, debug bool, validatorWithdrawalCredentials []byte, mnemonic string) (bool, string, error) {
// If there are more than 24 words we treat the additional characters as the passphrase.
mnemonicParts := strings.Split(mnemonic, " ")
mnemonicPassphrase := ""
if len(mnemonicParts) > 24 {
mnemonic = strings.Join(mnemonicParts[:24], " ")
mnemonicPassphrase = strings.Join(mnemonicParts[24:], " ")
}
// Normalise the input.
mnemonic = string(norm.NFKD.Bytes([]byte(mnemonic)))
mnemonicPassphrase = string(norm.NFKD.Bytes([]byte(mnemonicPassphrase)))
if !bip39.IsMnemonicValid(mnemonic) {
return false, "", errors.New("mnemonic is invalid")
}
// Create seed from mnemonic and passphrase.
seed := bip39.NewSeed(mnemonic, mnemonicPassphrase)
// Check first 1024 indices.
for i := 0; i < 1024; i++ {
path := fmt.Sprintf("m/12381/3600/%d/0", i)
if debug {
fmt.Printf("Checking path %s\n", path)
}
key, err := util.PrivateKeyFromSeedAndPath(seed, path)
if err != nil {
return false, "", errors.Wrap(err, "failed to generate key")
}
match, err := checkPrivKey(ctx, debug, validatorWithdrawalCredentials, key)
if err != nil {
return false, "", errors.Wrap(err, "failed to match key")
}
if match {
return true, path, nil
}
}
return false, "", nil
}

View File

@@ -0,0 +1,51 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorkeycheck
import (
"context"
"fmt"
"os"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// 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 "", err
}
results, exitCode, err := output(ctx, dataOut)
if err != nil {
return "", err
}
if exitCode != 0 {
fmt.Println(results)
os.Exit(exitCode)
}
return results, nil
}

View File

@@ -39,7 +39,7 @@ In quiet mode this will return 0 if the the duties have been obtained, otherwise
if viper.GetBool("quiet") {
return nil
}
fmt.Printf(res)
fmt.Print(res)
return nil
},
}

67
cmd/validatorkeycheck.go Normal file
View File

@@ -0,0 +1,67 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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"
validatorkeycheck "github.com/wealdtech/ethdo/cmd/validator/keycheck"
)
var validatorKeycheckCmd = &cobra.Command{
Use: "keycheck",
Short: "Check that the withdrawal credentials for a validator matches the given key.",
Long: `Check that the withdrawal credentials for a validator matches the given key. For example:
ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --privkey=0x1b46e61babc7a6a0fbfe8e416de3c71f85e367f24e0bfcb12e57adb11117662c
A mnemonic can be used in place of a private key, in which case the first 1,024 indices of the standard withdrawal key path will be scanned for a matching key.
In quiet mode this will return 0 if the withdrawal credentials match the key, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorkeycheck.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
validatorCmd.AddCommand(validatorKeycheckCmd)
validatorFlags(validatorKeycheckCmd)
validatorKeycheckCmd.Flags().String("withdrawal-credentials", "", "Withdrawal credentials to check (can run offline)")
validatorKeycheckCmd.Flags().String("mnemonic", "", "Mnemonic from which to generate withdrawal credentials")
validatorKeycheckCmd.Flags().String("privkey", "", "Private key from which to generate withdrawal credentials")
}
func validatorKeycheckBindings() {
if err := viper.BindPFlag("withdrawal-credentials", validatorKeycheckCmd.Flags().Lookup("withdrawal-credentials")); err != nil {
panic(err)
}
if err := viper.BindPFlag("mnemonic", validatorKeycheckCmd.Flags().Lookup("mnemonic")); err != nil {
panic(err)
}
if err := viper.BindPFlag("privkey", validatorKeycheckCmd.Flags().Lookup("privkey")); err != nil {
panic(err)
}
}

View File

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

View File

@@ -15,7 +15,7 @@ The first thing you need to do is to create a wallet. To do this run the comman
- rename the wallet to something other than `Wallet` if you so desire. If so, you will need to change it in all subsequent commands
```
$ ethdo wallet create --type=hd --mnemonic='abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art' --wallet=Wallet --wallet-passphrase=secret
$ ethdo wallet create --type=hd --mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" --wallet=Wallet --wallet-passphrase=secret
```
### I want an account with a specific public key.
@@ -87,5 +87,5 @@ If you wish to have this data for a particular test network you will need to sup
It is possible to derive keys directly from a mnemonic and path without going through the interim steps. Note that this will _not_ create accounts, and cannot be used to then sign data or requests. This may or not be desirable, depending on your requirements.
```
$ ethdo account derive --mnemonic='abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art' --path=m/12381/3600/0/0
$ ethdo account derive --mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" --path=m/12381/3600/0/0
```

View File

@@ -325,6 +325,23 @@ Prior justified epoch: 3
Prior justified epoch distance: 4
```
#### `time`
`ethdo chain time` calculates the time period of Ethereum 2 epochs and slots. Options include:
- `epoch` show epoch and slot times for the given epoch
- `slot` show epoch and slot times for the given slot
- `timestamp` show epoch and slot times for the given timestamp
```sh
$ ethdo chain time --epoch=1234
Epoch 1234
Epoch start 2020-12-06 23:37:59
Epoch end 2020-12-06 23:44:23
Slot 39488
Slot start 2020-12-06 23:37:59
Slot end 2020-12-06 23:38:11
```
### `deposit` comands
Deposit commands focus on information about deposit data information in a JSON file generated by the `ethdo validator depositdata` command.
@@ -471,6 +488,18 @@ Balance: 3.201850307 Ether
Effective balance: 3.1 Ether
```
#### `keycheck`
`ethdo validator keycheck` checks if a given key matches a validator's withdrawal credentials. Options include:
- `withdrawal-credentials` the withdrawal credentials against which to match
- `privkey` the private key used to generat matching withdrawal credentials
- `mnemonic` the mnemonic used to generate matching withdrawal credentials
```sh
$ ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --mnemonic='abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art'
Withdrawal credentials confirmed at path m/12381/3600/10/0
```
### `attester` commands
Attester commands focus on Ethereum 2 validators' actions as attesters.