mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
384ee3dcaa | ||
|
|
3e8b1a6dad | ||
|
|
d2dec4a444 | ||
|
|
7e171bdb1e | ||
|
|
0cedf79a89 | ||
|
|
65ad1248ce | ||
|
|
e1180f97ce |
@@ -1,3 +1,9 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
141
cmd/chain/eth1votes/process.go
Normal file
141
cmd/chain/eth1votes/process.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// 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.
|
||||
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch+1)-1))
|
||||
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":
|
||||
|
||||
@@ -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.22.0)"
|
||||
var ReleaseVersion = "local build (latest release 1.23.0)"
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user