Compare commits

...

12 Commits

Author SHA1 Message Date
Jim McDonald
0bdf68edf6 Do not fetch future states. 2022-06-01 12:42:40 +01:00
Jim McDonald
b24341b7da Do not fetch future states. 2022-06-01 12:42:12 +01:00
Jim McDonald
384ee3dcaa Bump version. 2022-06-01 12:22:23 +01:00
Jim McDonald
3e8b1a6dad Add "chain eth1votes". 2022-06-01 12:21:40 +01:00
Jim McDonald
d2dec4a444 Tidy up formatting. 2022-05-31 13:39:36 +01:00
Jim McDonald
7e171bdb1e Provide more epoch summary information. 2022-05-30 21:52:30 +01:00
Jim McDonald
0cedf79a89 Bump version. 2022-05-30 16:08:58 +01:00
Jim McDonald
65ad1248ce epoch summary no sync committee information pre-Altair. 2022-05-30 16:08:31 +01:00
Jim McDonald
e1180f97ce error on attester inclusion without required params. 2022-05-26 22:29:21 +01:00
Jim McDonald
394b4a7cd2 Bump version. 2022-05-19 13:00:38 +01:00
Jim McDonald
fd574aae34 Tidy up tests. 2022-05-19 13:00:05 +01:00
Jim McDonald
7fe503f51d Add ropsten support. 2022-05-19 12:59:48 +01:00
20 changed files with 834 additions and 67 deletions

View File

@@ -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"

View File

@@ -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() {

View 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
}

View 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)
}
})
}
}

View 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
}

View 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
}

View 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)
}
})
}
}

View 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
View 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)
}
}

View File

@@ -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"),

View File

@@ -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)))
}
}
}

View File

@@ -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
}

View File

@@ -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":

View File

@@ -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 {

View File

@@ -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")
}

View File

@@ -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{

View File

@@ -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))

View File

@@ -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 {

View File

@@ -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`

View File

@@ -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.