mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bdf68edf6 | ||
|
|
b24341b7da | ||
|
|
384ee3dcaa | ||
|
|
3e8b1a6dad | ||
|
|
d2dec4a444 | ||
|
|
7e171bdb1e | ||
|
|
0cedf79a89 | ||
|
|
65ad1248ce | ||
|
|
e1180f97ce | ||
|
|
394b4a7cd2 | ||
|
|
fd574aae34 | ||
|
|
7fe503f51d |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,3 +1,15 @@
|
||||
1.23.1:
|
||||
- do not fetch future state for chain eth1votes
|
||||
|
||||
1.23.0:
|
||||
- do not fetch sync committee information for epoch summaries prior to Altair
|
||||
- ensure that "attester inclusion" without validator returns appropriate error
|
||||
- provide more information in "epoch summary" with verbose flag
|
||||
- add "chain eth1votes"
|
||||
|
||||
1.22.0:
|
||||
- add "ropsten" to the list of supported networks
|
||||
|
||||
1.21.0:
|
||||
- add "validator credentials get"
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func init() {
|
||||
attesterFlags(attesterInclusionCmd)
|
||||
attesterInclusionCmd.Flags().Int64("epoch", -1, "the last complete epoch")
|
||||
attesterInclusionCmd.Flags().String("pubkey", "", "the public key of the attester")
|
||||
attesterInclusionCmd.Flags().Int64("index", -1, "the index of the attester")
|
||||
attesterInclusionCmd.Flags().String("index", "", "the index of the attester")
|
||||
}
|
||||
|
||||
func attesterInclusionBindings() {
|
||||
|
||||
86
cmd/chain/eth1votes/command.go
Normal file
86
cmd/chain/eth1votes/command.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
json bool
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Input.
|
||||
epoch string
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
beaconStateProvider eth2client.BeaconStateProvider
|
||||
slotsPerEpoch uint64
|
||||
epochsPerEth1VotingPeriod uint64
|
||||
|
||||
// Output.
|
||||
slot phase0.Slot
|
||||
period uint64
|
||||
incumbent *phase0.ETH1Data
|
||||
eth1DataVotes []*phase0.ETH1Data
|
||||
votes map[string]*vote
|
||||
}
|
||||
|
||||
type vote struct {
|
||||
Vote *phase0.ETH1Data `json:"vote"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
json: viper.GetBool("json"),
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
if viper.GetString("epoch") != "" {
|
||||
c.epoch = viper.GetString("epoch")
|
||||
}
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
72
cmd/chain/eth1votes/command_internal_test.go
Normal file
72
cmd/chain/eth1votes/command_internal_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"data": "{}",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
_, err := newCommand(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
128
cmd/chain/eth1votes/output.go
Normal file
128
cmd/chain/eth1votes/output.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
)
|
||||
|
||||
type jsonOutput struct {
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Period uint64 `json:"period"`
|
||||
Incumbent *phase0.ETH1Data `json:"incumbent"`
|
||||
Votes []*vote `json:"votes"`
|
||||
}
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.json {
|
||||
return c.outputJSON(ctx)
|
||||
}
|
||||
return c.outputText(ctx)
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(ctx context.Context) (string, error) {
|
||||
votes := make([]*vote, 0, len(c.votes))
|
||||
totalVotes := 0
|
||||
for _, vote := range c.votes {
|
||||
votes = append(votes, vote)
|
||||
totalVotes += vote.Count
|
||||
}
|
||||
sort.Slice(votes, func(i int, j int) bool {
|
||||
if votes[i].Count != votes[j].Count {
|
||||
return votes[i].Count > votes[j].Count
|
||||
}
|
||||
return votes[i].Vote.DepositCount < votes[j].Vote.DepositCount
|
||||
})
|
||||
|
||||
output := &jsonOutput{
|
||||
Slot: c.slot,
|
||||
Period: c.period,
|
||||
Incumbent: c.incumbent,
|
||||
Votes: votes,
|
||||
}
|
||||
data, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *command) outputText(ctx context.Context) (string, error) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
if c.verbose {
|
||||
builder.WriteString("Slot: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", c.slot))
|
||||
}
|
||||
|
||||
builder.WriteString("Voting period: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", c.period))
|
||||
|
||||
if c.verbose {
|
||||
builder.WriteString("Incumbent: ")
|
||||
builder.WriteString(fmt.Sprintf("block %#x, deposit count %d\n", c.incumbent.BlockHash, c.incumbent.DepositCount))
|
||||
}
|
||||
|
||||
votes := make([]*vote, 0, len(c.votes))
|
||||
totalVotes := 0
|
||||
for _, vote := range c.votes {
|
||||
votes = append(votes, vote)
|
||||
totalVotes += vote.Count
|
||||
}
|
||||
sort.Slice(votes, func(i int, j int) bool {
|
||||
if votes[i].Count != votes[j].Count {
|
||||
return votes[i].Count > votes[j].Count
|
||||
}
|
||||
return votes[i].Vote.DepositCount < votes[j].Vote.DepositCount
|
||||
})
|
||||
|
||||
slot := c.chainTime.CurrentSlot()
|
||||
if slot > c.slot {
|
||||
slot = c.slot
|
||||
}
|
||||
|
||||
builder.WriteString("Slots through period: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", slot-phase0.Slot(c.period*(c.slotsPerEpoch*c.epochsPerEth1VotingPeriod))))
|
||||
|
||||
builder.WriteString("Votes this period: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", totalVotes))
|
||||
|
||||
if len(votes) > 0 {
|
||||
if c.verbose {
|
||||
for _, vote := range votes {
|
||||
builder.WriteString(fmt.Sprintf(" block %#x, deposit count %d: %d vote", vote.Vote.BlockHash, vote.Vote.DepositCount, vote.Count))
|
||||
if vote.Count != 1 {
|
||||
builder.WriteString("s\n")
|
||||
} else {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
builder.WriteString(fmt.Sprintf("Leading vote is for block %#x with %d votes\n", votes[0].Vote.BlockHash, votes[0].Count))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(builder.String(), "\n"), nil
|
||||
}
|
||||
145
cmd/chain/eth1votes/process.go
Normal file
145
cmd/chain/eth1votes/process.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
// Obtain information we need to process.
|
||||
if err := c.setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
epoch, err := util.ParseEpoch(ctx, c.chainTime, c.epoch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Need to fetch the state from the last slot of the epoch.
|
||||
fetchSlot := c.chainTime.FirstSlotOfEpoch(epoch+1) - 1
|
||||
if fetchSlot > c.chainTime.CurrentSlot() {
|
||||
fetchSlot = c.chainTime.CurrentSlot()
|
||||
}
|
||||
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", fetchSlot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain state")
|
||||
}
|
||||
if state == nil {
|
||||
return errors.New("state not returned by beacon node")
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
data, err := json.Marshal(state)
|
||||
if err == nil {
|
||||
fmt.Printf("%s\n", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
switch state.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
c.slot = phase0.Slot(state.Phase0.Slot)
|
||||
c.incumbent = state.Phase0.ETH1Data
|
||||
c.eth1DataVotes = state.Phase0.ETH1DataVotes
|
||||
case spec.DataVersionAltair:
|
||||
c.slot = phase0.Slot(state.Altair.Slot)
|
||||
c.incumbent = state.Altair.ETH1Data
|
||||
c.eth1DataVotes = state.Altair.ETH1DataVotes
|
||||
case spec.DataVersionBellatrix:
|
||||
c.slot = phase0.Slot(state.Bellatrix.Slot)
|
||||
c.incumbent = state.Bellatrix.ETH1Data
|
||||
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
|
||||
default:
|
||||
return fmt.Errorf("unhandled beacon state version %v", state.Version)
|
||||
}
|
||||
|
||||
c.period = uint64(c.slot) / (c.slotsPerEpoch * c.epochsPerEth1VotingPeriod)
|
||||
|
||||
c.votes = make(map[string]*vote)
|
||||
for _, eth1Vote := range c.eth1DataVotes {
|
||||
key := fmt.Sprintf("%#x:%d", eth1Vote.BlockHash, eth1Vote.DepositCount)
|
||||
if _, exists := c.votes[key]; !exists {
|
||||
c.votes[key] = &vote{
|
||||
Vote: eth1Vote,
|
||||
}
|
||||
}
|
||||
c.votes[key].Count++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) setup(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// Connect to the client.
|
||||
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to beacon node")
|
||||
}
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
var isProvider bool
|
||||
c.beaconStateProvider, isProvider = c.eth2Client.(eth2client.BeaconStateProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide beacon state")
|
||||
}
|
||||
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide spec information")
|
||||
}
|
||||
|
||||
spec, err := specProvider.Spec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
|
||||
tmp, exists := spec["SLOTS_PER_EPOCH"]
|
||||
if !exists {
|
||||
return errors.New("spec did not contain SLOTS_PER_EPOCH")
|
||||
}
|
||||
var good bool
|
||||
c.slotsPerEpoch, good = tmp.(uint64)
|
||||
if !good {
|
||||
return errors.New("SLOTS_PER_EPOCH value invalid")
|
||||
}
|
||||
tmp, exists = spec["EPOCHS_PER_ETH1_VOTING_PERIOD"]
|
||||
if !exists {
|
||||
return errors.New("spec did not contain EPOCHS_PER_ETH1_VOTING_PERIOD")
|
||||
}
|
||||
c.epochsPerEth1VotingPeriod, good = tmp.(uint64)
|
||||
if !good {
|
||||
return errors.New("EPOCHS_PER_ETH1_VOTING_PERIOD value invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
66
cmd/chain/eth1votes/process_internal_test.go
Normal file
66
cmd/chain/eth1votes/process_internal_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/viper"
|
||||
"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")
|
||||
}
|
||||
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "InvalidEpoch",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"epoch": "invalid",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "failed to parse epoch: strconv.ParseInt: parsing \"invalid\": invalid syntax",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
cmd, err := newCommand(context.Background())
|
||||
require.NoError(t, err)
|
||||
err = cmd.process(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/chain/eth1votes/run.go
Normal file
50
cmd/chain/eth1votes/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := newCommand(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to set up command")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
if err := c.process(ctx); err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := c.output(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
63
cmd/chaineth1votes.go
Normal file
63
cmd/chaineth1votes.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright © 2022 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"
|
||||
chaineth1votes "github.com/wealdtech/ethdo/cmd/chain/eth1votes"
|
||||
)
|
||||
|
||||
var chainEth1VotesCmd = &cobra.Command{
|
||||
Use: "eth1votes",
|
||||
Short: "Show chain execution votes",
|
||||
Long: `Show beacon chain execution votes. For example:
|
||||
|
||||
ethdo chain eth1votes
|
||||
|
||||
Note that this will fetch the votes made in blocks up to the end of the provided epoch.
|
||||
|
||||
In quiet mode this will return 0 if there is a majority for the votes, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := chaineth1votes.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
chainCmd.AddCommand(chainEth1VotesCmd)
|
||||
chainFlags(chainEth1VotesCmd)
|
||||
chainEth1VotesCmd.Flags().String("epoch", "", "epoch for which to fetch the votes")
|
||||
chainEth1VotesCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
}
|
||||
|
||||
func chainEth1VotesBindings() {
|
||||
if err := viper.BindPFlag("epoch", chainEth1VotesCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", chainEth1VotesCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -40,22 +40,27 @@ type command struct {
|
||||
jsonOutput bool
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
proposerDutiesProvider eth2client.ProposerDutiesProvider
|
||||
blocksProvider eth2client.SignedBeaconBlockProvider
|
||||
syncCommitteesProvider eth2client.SyncCommitteesProvider
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
proposerDutiesProvider eth2client.ProposerDutiesProvider
|
||||
blocksProvider eth2client.SignedBeaconBlockProvider
|
||||
syncCommitteesProvider eth2client.SyncCommitteesProvider
|
||||
validatorsProvider eth2client.ValidatorsProvider
|
||||
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
|
||||
|
||||
// Results.
|
||||
summary *epochSummary
|
||||
}
|
||||
|
||||
type epochSummary struct {
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
FirstSlot phase0.Slot `json:"first_slot"`
|
||||
LastSlot phase0.Slot `json:"last_slot"`
|
||||
Proposals []*epochProposal `json:"proposals"`
|
||||
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
FirstSlot phase0.Slot `json:"first_slot"`
|
||||
LastSlot phase0.Slot `json:"last_slot"`
|
||||
Proposals []*epochProposal `json:"proposals"`
|
||||
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
|
||||
ActiveValidators int `json:"active_validators"`
|
||||
ParticipatingValidators int `json:"participating_validators"`
|
||||
NonParticipatingValidators []*nonParticipatingValidator `json:"nonparticipating_validators"`
|
||||
}
|
||||
|
||||
type epochProposal struct {
|
||||
@@ -69,6 +74,12 @@ type epochSyncCommittee struct {
|
||||
Missed int `json:"missed"`
|
||||
}
|
||||
|
||||
type nonParticipatingValidator struct {
|
||||
Validator phase0.ValidatorIndex `json:"validator_index"`
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Committee phase0.CommitteeIndex `json:"committee_index"`
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
|
||||
@@ -47,58 +47,56 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
builder.WriteString(fmt.Sprintf("%d:\n", c.summary.Epoch))
|
||||
|
||||
proposedBlocks := 0
|
||||
missedProposals := make([]string, 0, len(c.summary.Proposals))
|
||||
for _, proposal := range c.summary.Proposals {
|
||||
if !proposal.Block {
|
||||
missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.Proposer))
|
||||
} else {
|
||||
proposedBlocks++
|
||||
}
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf(" Proposals: %d/%d (%0.2f%%)", proposedBlocks, len(missedProposals)+proposedBlocks, 100.0*float64(proposedBlocks)/float64(len(missedProposals)+proposedBlocks)))
|
||||
if c.verbose {
|
||||
for _, proposal := range c.summary.Proposals {
|
||||
builder.WriteString(" Slot ")
|
||||
builder.WriteString(fmt.Sprintf("%d (%d/%d):\n", proposal.Slot, uint64(proposal.Slot)%uint64(len(c.summary.Proposals)), len(c.summary.Proposals)))
|
||||
builder.WriteString(" Proposer: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", proposal.Proposer))
|
||||
builder.WriteString(" Proposed: ")
|
||||
if proposal.Block {
|
||||
proposedBlocks++
|
||||
builder.WriteString("✓\n")
|
||||
} else {
|
||||
builder.WriteString("✕\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
missedProposals := make([]string, 0, len(c.summary.Proposals))
|
||||
for _, proposal := range c.summary.Proposals {
|
||||
if !proposal.Block {
|
||||
missedProposals = append(missedProposals, fmt.Sprintf(" Slot %d (validator %d)\n", proposal.Slot, proposal.Proposer))
|
||||
} else {
|
||||
proposedBlocks++
|
||||
}
|
||||
}
|
||||
if len(missedProposals) > 0 {
|
||||
builder.WriteString(" Missed proposals:\n")
|
||||
for _, missedProposal := range missedProposals {
|
||||
builder.WriteString(missedProposal)
|
||||
continue
|
||||
}
|
||||
builder.WriteString("\n Slot ")
|
||||
builder.WriteString(fmt.Sprintf("%d (%d/%d)", proposal.Slot, uint64(proposal.Slot)%uint64(len(c.summary.Proposals)), len(c.summary.Proposals)))
|
||||
builder.WriteString(" validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d", proposal.Proposer))
|
||||
builder.WriteString(" not proposed or not included")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString(fmt.Sprintf("\n Attestations: %d/%d (%0.2f%%)", c.summary.ParticipatingValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.ParticipatingValidators)/float64(c.summary.ActiveValidators)))
|
||||
if c.verbose {
|
||||
for _, syncCommittee := range c.summary.SyncCommittee {
|
||||
builder.WriteString(" Sync committee validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d:\n", syncCommittee.Index))
|
||||
builder.WriteString(" Chances: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", proposedBlocks))
|
||||
builder.WriteString(" Included: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", proposedBlocks-syncCommittee.Missed))
|
||||
builder.WriteString(" Inclusion %: ")
|
||||
builder.WriteString(fmt.Sprintf("%0.2f\n", 100.0*float64(proposedBlocks-syncCommittee.Missed)/float64(proposedBlocks)))
|
||||
// Sort list by validator index.
|
||||
for _, validator := range c.summary.NonParticipatingValidators {
|
||||
builder.WriteString("\n Slot ")
|
||||
builder.WriteString(fmt.Sprintf("%d", validator.Slot))
|
||||
builder.WriteString(" committee ")
|
||||
builder.WriteString(fmt.Sprintf("%d", validator.Committee))
|
||||
builder.WriteString(" validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d", validator.Validator))
|
||||
builder.WriteString(" failed to participate")
|
||||
}
|
||||
} else {
|
||||
missedSyncCommittees := make([]string, 0, len(c.summary.SyncCommittee))
|
||||
for _, syncCommittee := range c.summary.SyncCommittee {
|
||||
missedPct := 100.0 * float64(syncCommittee.Missed) / float64(proposedBlocks)
|
||||
missedSyncCommittees = append(missedSyncCommittees, fmt.Sprintf(" %d (%0.2f%%) by validator %d\n", syncCommittee.Missed, missedPct, syncCommittee.Index))
|
||||
}
|
||||
|
||||
if c.summary.Epoch >= c.chainTime.AltairInitialEpoch() {
|
||||
contributions := proposedBlocks * 512 // SYNC_COMMITTEE_SIZE
|
||||
totalMissed := 0
|
||||
for _, contribution := range c.summary.SyncCommittee {
|
||||
totalMissed += contribution.Missed
|
||||
}
|
||||
if len(missedSyncCommittees) > 0 {
|
||||
builder.WriteString(" Missed sync committees (excluding missed blocks):\n")
|
||||
for _, missedSyncCommittee := range missedSyncCommittees {
|
||||
builder.WriteString(missedSyncCommittee)
|
||||
builder.WriteString(fmt.Sprintf("\n Sync committees: %d/%d (%0.2f%%)", contributions-totalMissed, contributions, 100.0*float64(contributions-totalMissed)/float64(contributions)))
|
||||
if c.verbose {
|
||||
for _, syncCommittee := range c.summary.SyncCommittee {
|
||||
builder.WriteString("\n Validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d", syncCommittee.Index))
|
||||
builder.WriteString(" included ")
|
||||
builder.WriteString(fmt.Sprintf("%d/%d", proposedBlocks-syncCommittee.Missed, proposedBlocks))
|
||||
builder.WriteString(fmt.Sprintf(" (%0.2f%%)", 100.0*float64(proposedBlocks-syncCommittee.Missed)/float64(proposedBlocks)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ package epochsummary
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
@@ -79,17 +81,112 @@ func (c *command) processProposerDuties(ctx context.Context) error {
|
||||
|
||||
func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
// Obtain all active validators for the given epoch.
|
||||
// Do in future.
|
||||
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validators for epoch")
|
||||
}
|
||||
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
||||
for _, validator := range validators {
|
||||
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
|
||||
activeValidators[validator.Index] = validator
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain number of validators that voted for blocks in the epoch.
|
||||
// These votes can be included anywhere from the second slot of
|
||||
// the epoch to the first slot of the next-but-one epoch.
|
||||
firstSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) + 1
|
||||
lastSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch + 2)
|
||||
if lastSlot > c.chainTime.CurrentSlot() {
|
||||
lastSlot = c.chainTime.CurrentSlot()
|
||||
}
|
||||
|
||||
votes := make(map[phase0.ValidatorIndex]struct{})
|
||||
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
participations := make(map[phase0.ValidatorIndex]*nonParticipatingValidator)
|
||||
|
||||
for slot := firstSlot; slot <= lastSlot; slot++ {
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
|
||||
}
|
||||
if block == nil {
|
||||
// No block at this slot; that's fine.
|
||||
continue
|
||||
}
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, attestation := range attestations {
|
||||
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
|
||||
// Outside of this epoch's range.
|
||||
continue
|
||||
}
|
||||
slotCommittees, exists := allCommittees[attestation.Data.Slot]
|
||||
if !exists {
|
||||
beaconCommittees, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", attestation.Data.Slot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
|
||||
}
|
||||
for _, beaconCommittee := range beaconCommittees {
|
||||
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
|
||||
allCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
}
|
||||
allCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
|
||||
for _, index := range beaconCommittee.Validators {
|
||||
participations[index] = &nonParticipatingValidator{
|
||||
Validator: index,
|
||||
Slot: beaconCommittee.Slot,
|
||||
Committee: beaconCommittee.Index,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
slotCommittees = allCommittees[attestation.Data.Slot]
|
||||
}
|
||||
committee := slotCommittees[attestation.Data.Index]
|
||||
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
|
||||
if attestation.AggregationBits.BitAt(i) {
|
||||
votes[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.summary.ActiveValidators = len(activeValidators)
|
||||
c.summary.ParticipatingValidators = len(votes)
|
||||
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes))
|
||||
for activeValidatorIndex := range activeValidators {
|
||||
if _, exists := votes[activeValidatorIndex]; !exists {
|
||||
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, participations[activeValidatorIndex])
|
||||
}
|
||||
}
|
||||
sort.Slice(c.summary.NonParticipatingValidators, func(i int, j int) bool {
|
||||
if c.summary.NonParticipatingValidators[i].Slot != c.summary.NonParticipatingValidators[j].Slot {
|
||||
return c.summary.NonParticipatingValidators[i].Slot < c.summary.NonParticipatingValidators[j].Slot
|
||||
}
|
||||
if c.summary.NonParticipatingValidators[i].Committee != c.summary.NonParticipatingValidators[j].Committee {
|
||||
return c.summary.NonParticipatingValidators[i].Committee < c.summary.NonParticipatingValidators[j].Committee
|
||||
}
|
||||
return c.summary.NonParticipatingValidators[i].Validator < c.summary.NonParticipatingValidators[j].Validator
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
if c.summary.Epoch < c.chainTime.AltairInitialEpoch() {
|
||||
// The epoch is pre-Altair. No info but no error.
|
||||
return nil
|
||||
}
|
||||
|
||||
committee, err := c.syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", c.summary.FirstSlot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain sync committee")
|
||||
}
|
||||
if len(committee.Validators) == 0 {
|
||||
return errors.New("empty sync committee")
|
||||
return errors.Wrap(err, "empty sync committee")
|
||||
}
|
||||
|
||||
missed := make(map[phase0.ValidatorIndex]int)
|
||||
@@ -135,6 +232,16 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(c.summary.SyncCommittee, func(i int, j int) bool {
|
||||
missedDiff := c.summary.SyncCommittee[i].Missed - c.summary.SyncCommittee[j].Missed
|
||||
if missedDiff != 0 {
|
||||
// Actually want to order by missed descending, so invert the expected condition.
|
||||
return missedDiff > 0
|
||||
}
|
||||
// Then order by validator index.
|
||||
return c.summary.SyncCommittee[i].Index < c.summary.SyncCommittee[j].Index
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -169,6 +276,14 @@ func (c *command) setup(ctx context.Context) error {
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide sync committee duties")
|
||||
}
|
||||
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide validators")
|
||||
}
|
||||
c.beaconCommitteesProvider, isProvider = c.eth2Client.(eth2client.BeaconCommitteesProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide beacon committees")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
blockAnalyzeBindings()
|
||||
case "block/info":
|
||||
blockInfoBindings()
|
||||
case "chain/eth1votes":
|
||||
chainEth1VotesBindings()
|
||||
case "chain/queues":
|
||||
chainQueuesBindings()
|
||||
case "chain/time":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2022 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
|
||||
@@ -110,6 +110,7 @@ func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) {
|
||||
[4]byte{0x00, 0x00, 0x00, 0x00}: "mainnet",
|
||||
[4]byte{0x00, 0x00, 0x20, 0x09}: "pyrmont",
|
||||
[4]byte{0x00, 0x00, 0x10, 0x20}: "prater",
|
||||
[4]byte{0x80, 0x00, 0x00, 0x69}: "ropsten",
|
||||
}
|
||||
|
||||
if datum.validatorPubKey == nil {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2022 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
|
||||
@@ -55,7 +55,7 @@ func init() {
|
||||
validatorDepositDataCmd.Flags().String("withdrawaladdress", "", "Ethereum 1 address of the account to which the validator funds will be withdrawn")
|
||||
validatorDepositDataCmd.Flags().String("depositvalue", "", "Value of the amount to be deposited")
|
||||
validatorDepositDataCmd.Flags().Bool("raw", false, "Print raw deposit data transaction data")
|
||||
validatorDepositDataCmd.Flags().String("forkversion", "", "Use a hard-coded fork version (default is to fetch it from the node)")
|
||||
validatorDepositDataCmd.Flags().String("forkversion", "", "Use a hard-coded fork version (default is to use mainnet value)")
|
||||
validatorDepositDataCmd.Flags().Bool("launchpad", false, "Print launchpad-compatible JSON")
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
|
||||
// ReleaseVersion is the release version of the codebase.
|
||||
// Usually overridden by tag names when building binaries.
|
||||
var ReleaseVersion = "local build (latest release 1.21.0)"
|
||||
var ReleaseVersion = "local build (latest release 1.23.1)"
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
@@ -35,11 +35,11 @@ import (
|
||||
func TestInput(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
dir := os.TempDir()
|
||||
datFile := filepath.Join(dir, "backup.dat")
|
||||
err := ioutil.WriteFile(datFile, []byte("dummy"), 0600)
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
// defer os.RemoveAll(dir)
|
||||
datFile := filepath.Join(dir, "backup.dat")
|
||||
require.NoError(t, ioutil.WriteFile(datFile, []byte("dummy"), 0600))
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
|
||||
@@ -37,10 +37,10 @@ func TestProcess(t *testing.T) {
|
||||
"ed2166659f7b5412a169ec83627386bc6ff1a31e67735d405b2bf7cb122ad7ced35c87e42c8e8f7ba90b5899a94be506687a9c5b353af2a216018d9f1bf61745a5",
|
||||
}
|
||||
|
||||
dir := os.TempDir()
|
||||
datFile := filepath.Join(dir, "backup.dat")
|
||||
err := ioutil.WriteFile(datFile, export, 0600)
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
datFile := filepath.Join(dir, "backup.dat")
|
||||
require.NoError(t, ioutil.WriteFile(datFile, export, 0600))
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -337,6 +337,22 @@ Voluntary exits: 0
|
||||
|
||||
Chain commands focus on providing information about Ethereum 2 chains.
|
||||
|
||||
#### `eth1votes`
|
||||
|
||||
`ethdo chain eth1votes` obtains information about the votes for the next Ethereum 1 block to be incorporated in to the chain for deposits. Options include:
|
||||
- `epoch` show the votes at the end of the given epoch
|
||||
- `json` provide JSON output
|
||||
|
||||
```sh
|
||||
$ ethdo chain eth1votes
|
||||
Voting period: 6
|
||||
Slots through period: 1000
|
||||
Votes this period: 959
|
||||
Leading vote is for block 0x0ae5716ac1906592dbfb243ccadf90191f706d6f8c925b4f2712d2e24687553a with 356 votes
|
||||
```
|
||||
|
||||
Additional information is supplied when using `--verbose`
|
||||
|
||||
#### `info`
|
||||
|
||||
`ethdo chain info` obtains information about an Ethereum 2 chain.
|
||||
@@ -575,10 +591,11 @@ $ ethdo validator credentials get --account=Validators/1
|
||||
|
||||
`ethdo validator depositdata` generates the data required to deposit one or more Ethereum 2 validators. Options include:
|
||||
- `withdrawalaccount` specify the account to be used for the withdrawal credentials (if withdrawalpubkey is not supplied)
|
||||
- `withdrawaladdress` specify the Ethereum execution address to be used for the withdrawal credentials (if withdrawalpubkey is not supplied)
|
||||
- `withdrawalpubkey` specify the public key to be used for the withdrawal credentials (if withdrawalaccount is not supplied)
|
||||
- `validatoraccount` specify the account to be used for the validator
|
||||
- `depositvalue` specify the amount of the deposit
|
||||
- `forkversion` specify the fork version for the deposit signature; this should not be included unless the deposit is being generated offline. Note that supplying an incorrect value could result in the loss of your deposit, so only supply this value if you are sure you know what you are doing
|
||||
- `forkversion` specify the fork version for the deposit signature; this defaults to mainnet. Note that supplying an incorrect value could result in the loss of your deposit, so only supply this value if you are sure you know what you are doing. You can find the value for other chains by fetching the value supplied in "Genesis fork version" of the `ethdo chain info` command
|
||||
- `raw` generate raw hex output that can be supplied as the data to an Ethereum 1 deposit transaction
|
||||
|
||||
#### `exit`
|
||||
|
||||
@@ -27,6 +27,7 @@ var networks = map[string]string{
|
||||
"07b39f4fde4a38bace212b546dac87c58dfe3fdc": "Medalla",
|
||||
"8c5fecdc472e27bc447696f431e425d02dd46a8c": "Pyrmont",
|
||||
"ff50ed3d0ec03ac01d4c79aad74928bff48a7b2b": "Prater",
|
||||
"6f22ffbc56eff051aecf839396dd1ed9ad6bba9d": "Ropsten",
|
||||
}
|
||||
|
||||
// Network returns the name of the network., calculated from the deposit contract information.
|
||||
|
||||
Reference in New Issue
Block a user