Skip to content

Commit

Permalink
Add "chain eth1votes".
Browse files Browse the repository at this point in the history
  • Loading branch information
mcdee committed Jun 1, 2022
1 parent d2dec4a commit 3e8b1a6
Show file tree
Hide file tree
Showing 10 changed files with 626 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
dev:
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
Expand Down
86 changes: 86 additions & 0 deletions cmd/chain/eth1votes/command.go
Original file line number Diff line number Diff line change
@@ -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 changes: 72 additions & 0 deletions cmd/chain/eth1votes/command_internal_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 128 additions & 0 deletions cmd/chain/eth1votes/output.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 3e8b1a6

Please sign in to comment.