mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-11 06:58:02 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97fa04a7b2 | ||
|
|
4977ee82e5 | ||
|
|
090680366c | ||
|
|
531c86847f | ||
|
|
446e437531 | ||
|
|
63d8ccf1a0 | ||
|
|
77abe0e158 | ||
|
|
547f8d9e71 | ||
|
|
e144217f25 | ||
|
|
d919810ce1 | ||
|
|
0bdf68edf6 | ||
|
|
b24341b7da | ||
|
|
384ee3dcaa | ||
|
|
3e8b1a6dad | ||
|
|
d2dec4a444 | ||
|
|
7e171bdb1e | ||
|
|
0cedf79a89 | ||
|
|
65ad1248ce | ||
|
|
e1180f97ce | ||
|
|
394b4a7cd2 | ||
|
|
fd574aae34 | ||
|
|
7fe503f51d | ||
|
|
6bfb0ef098 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,3 +1,29 @@
|
||||
1.25.0:
|
||||
- add "proposer duties"
|
||||
- add deposit signature verification to "deposit verify"
|
||||
|
||||
1.24.1:
|
||||
- fix potential crash when new validators are activated
|
||||
- add "sepolia" to the list of supported networks
|
||||
|
||||
1.24.0:
|
||||
- add "validator yield"
|
||||
|
||||
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"
|
||||
|
||||
1.20.0:
|
||||
- add "chain queues"
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -40,24 +40,15 @@ func TestInput(t *testing.T) {
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"validators": "1",
|
||||
"timeout": "5s",
|
||||
"blockid": "1",
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "ValidatorsZero",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"validators": "0",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "validators must be at least 1",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"validators": "1",
|
||||
"blockid": "1",
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
|
||||
@@ -33,13 +33,13 @@ func TestProcess(t *testing.T) {
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "InvalidData",
|
||||
name: "NoBlock",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "60s",
|
||||
"validators": "1",
|
||||
"data": "[[",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"blockid": "invalid",
|
||||
},
|
||||
err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"Invalid block: invalid\"}",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
{
|
||||
name: "ConnectionBad",
|
||||
@@ -79,7 +79,7 @@ func TestInput(t *testing.T) {
|
||||
timeout: 5 * time.Second,
|
||||
blockID: "justified",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
|
||||
},
|
||||
{
|
||||
name: "BlockIDNil",
|
||||
|
||||
87
cmd/chain/eth1votes/command.go
Normal file
87
cmd/chain/eth1votes/command.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// 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.
|
||||
xepoch string
|
||||
xperiod string
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
beaconStateProvider eth2client.BeaconStateProvider
|
||||
slotsPerEpoch uint64
|
||||
epochsPerEth1VotingPeriod uint64
|
||||
|
||||
// Output.
|
||||
slot phase0.Slot
|
||||
epoch phase0.Epoch
|
||||
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")
|
||||
|
||||
c.xepoch = viper.GetString("epoch")
|
||||
c.xperiod = viper.GetString("period")
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
125
cmd/chain/eth1votes/output.go
Normal file
125
cmd/chain/eth1votes/output.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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 {
|
||||
Period uint64 `json:"period"`
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
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{
|
||||
Period: c.period,
|
||||
Epoch: c.epoch,
|
||||
Slot: c.slot,
|
||||
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{}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
slotsThroughPeriod := slot + 1 - phase0.Slot(c.period*(c.slotsPerEpoch*c.epochsPerEth1VotingPeriod))
|
||||
builder.WriteString("Slots through period: ")
|
||||
builder.WriteString(fmt.Sprintf("%d (%d)\n", slotsThroughPeriod, c.slot))
|
||||
|
||||
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")
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf(" (%0.2f%%)\n", 100.0*float64(vote.Count)/float64(slotsThroughPeriod)))
|
||||
}
|
||||
} else {
|
||||
builder.WriteString(fmt.Sprintf("Leading vote is for block %#x with %d votes (%0.2f%%)\n", votes[0].Vote.BlockHash, votes[0].Count, 100.0*float64(votes[0].Count)/float64(slotsThroughPeriod)))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(builder.String(), "\n"), nil
|
||||
}
|
||||
161
cmd/chain/eth1votes/process.go
Normal file
161
cmd/chain/eth1votes/process.go
Normal file
@@ -0,0 +1,161 @@
|
||||
// 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"
|
||||
"strconv"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var err error
|
||||
if c.xperiod != "" {
|
||||
period, err := strconv.ParseUint(c.xperiod, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.epoch = phase0.Epoch(c.epochsPerEth1VotingPeriod*(period+1)) - 1
|
||||
} else {
|
||||
c.epoch, err = util.ParseEpoch(ctx, c.chainTime, c.xepoch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Do not fetch from the future.
|
||||
if c.epoch > c.chainTime.CurrentEpoch() {
|
||||
c.epoch = c.chainTime.CurrentEpoch()
|
||||
}
|
||||
|
||||
// Need to fetch the state from the last slot of the epoch.
|
||||
fetchSlot := c.chainTime.FirstSlotOfEpoch(c.epoch+1) - 1
|
||||
// Do not fetch from the future.
|
||||
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.epoch) / 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
|
||||
}
|
||||
67
cmd/chaineth1votes.go
Normal file
67
cmd/chaineth1votes.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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().String("period", "", "period 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("period", chainEth1VotesCmd.Flags().Lookup("period")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", chainEth1VotesCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
@@ -219,15 +219,15 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
|
||||
outputIf(!quiet, "Validator public key verified")
|
||||
}
|
||||
|
||||
var pubKey spec.BLSPubKey
|
||||
var pubKey phase0.BLSPubKey
|
||||
copy(pubKey[:], deposit.PublicKey)
|
||||
var signature spec.BLSSignature
|
||||
var signature phase0.BLSSignature
|
||||
copy(signature[:], deposit.Signature)
|
||||
|
||||
depositData := &spec.DepositData{
|
||||
depositData := &phase0.DepositData{
|
||||
PublicKey: pubKey,
|
||||
WithdrawalCredentials: deposit.WithdrawalCredentials,
|
||||
Amount: spec.Gwei(deposit.Amount),
|
||||
Amount: phase0.Gwei(deposit.Amount),
|
||||
Signature: signature,
|
||||
}
|
||||
depositDataRoot, err := depositData.HashTreeRoot()
|
||||
@@ -248,7 +248,7 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
|
||||
}
|
||||
} else {
|
||||
if depositVerifyForkVersion == "" {
|
||||
outputIf(!quiet, "fork version not supplied; NOT checked")
|
||||
outputIf(!quiet, "fork version not supplied; not checked")
|
||||
} else {
|
||||
forkVersion, err := hex.DecodeString(strings.TrimPrefix(depositVerifyForkVersion, "0x"))
|
||||
if err != nil {
|
||||
@@ -260,6 +260,49 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
|
||||
outputIf(!quiet, "Fork version incorrect")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(deposit.DepositMessageRoot) != 32 {
|
||||
outputIf(!quiet, "Deposit message root not supplied; not checked")
|
||||
} else {
|
||||
// We can also verify the deposit message signature.
|
||||
depositMessage := &phase0.DepositMessage{
|
||||
PublicKey: pubKey,
|
||||
WithdrawalCredentials: withdrawalCredentials,
|
||||
Amount: phase0.Gwei(deposit.Amount),
|
||||
}
|
||||
depositMessageRoot, err := depositMessage.HashTreeRoot()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to generate deposit message root")
|
||||
}
|
||||
|
||||
domainBytes := e2types.Domain(e2types.DomainDeposit, forkVersion, e2types.ZeroGenesisValidatorsRoot)
|
||||
var domain phase0.Domain
|
||||
copy(domain[:], domainBytes)
|
||||
container := &phase0.SigningData{
|
||||
ObjectRoot: depositMessageRoot,
|
||||
Domain: domain,
|
||||
}
|
||||
containerRoot, err := container.HashTreeRoot()
|
||||
if err != nil {
|
||||
return false, errors.New("failed to generate root for container")
|
||||
}
|
||||
|
||||
validatorPubKey, err := e2types.BLSPublicKeyFromBytes(pubKey[:])
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to generate validator public key")
|
||||
}
|
||||
blsSig, err := e2types.BLSSignatureFromBytes(signature[:])
|
||||
if err != nil {
|
||||
return false, errors.New("failed to verify BLS signature")
|
||||
}
|
||||
signatureVerified := blsSig.Verify(containerRoot[:], validatorPubKey)
|
||||
if signatureVerified {
|
||||
outputIf(!quiet, "Deposit message signature verified")
|
||||
} else {
|
||||
outputIf(!quiet, "Deposit message signature NOT verified")
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,114 @@ 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 {
|
||||
if _, exists := participations[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 +234,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 +278,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
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
{
|
||||
name: "ConnectionBad",
|
||||
@@ -75,7 +75,7 @@ func TestInput(t *testing.T) {
|
||||
"connection": "localhost:1",
|
||||
"topics": []string{"one", "two"},
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
|
||||
},
|
||||
{
|
||||
name: "TopicsNil",
|
||||
|
||||
@@ -27,3 +27,6 @@ var proposerCmd = &cobra.Command{
|
||||
func init() {
|
||||
RootCmd.AddCommand(proposerCmd)
|
||||
}
|
||||
|
||||
func proposerFlags(cmd *cobra.Command) {
|
||||
}
|
||||
|
||||
80
cmd/proposer/duties/command.go
Normal file
80
cmd/proposer/duties/command.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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 proposerduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"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
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Operation.
|
||||
epoch string
|
||||
jsonOutput bool
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
proposerDutiesProvider eth2client.ProposerDutiesProvider
|
||||
|
||||
// Results.
|
||||
results *results
|
||||
}
|
||||
|
||||
type results struct {
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
Duties []*apiv1.ProposerDuty `json:"duties"`
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
results: &results{},
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
c.epoch = viper.GetString("epoch")
|
||||
c.jsonOutput = viper.GetBool("json")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
79
cmd/proposer/duties/command_internal_test.go
Normal file
79
cmd/proposer/duties/command_internal_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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 proposerduties
|
||||
|
||||
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",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GoodWithEpoch",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"epoch": "-1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
62
cmd/proposer/duties/output.go
Normal file
62
cmd/proposer/duties/output.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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 proposerduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.jsonOutput {
|
||||
return c.outputJSON(ctx)
|
||||
}
|
||||
|
||||
return c.outputTxt(ctx)
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(_ context.Context) (string, error) {
|
||||
data, err := json.Marshal(c.results)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("Epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d:\n", c.results.Epoch))
|
||||
|
||||
for _, duty := range c.results.Duties {
|
||||
builder.WriteString(" Slot ")
|
||||
builder.WriteString(fmt.Sprintf("%d: ", duty.Slot))
|
||||
builder.WriteString("validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d", duty.ValidatorIndex))
|
||||
if c.verbose {
|
||||
builder.WriteString(" (pubkey ")
|
||||
builder.WriteString(fmt.Sprintf("%#x)", duty.PubKey))
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(builder.String(), "\n"), nil
|
||||
}
|
||||
70
cmd/proposer/duties/process.go
Normal file
70
cmd/proposer/duties/process.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 proposerduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"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.
|
||||
err := c.setup(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.results.Epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epoch)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse epoch")
|
||||
}
|
||||
|
||||
c.results.Duties, err = c.proposerDutiesProvider.ProposerDuties(ctx, c.results.Epoch, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain proposer duties")
|
||||
}
|
||||
|
||||
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.proposerDutiesProvider, isProvider = c.eth2Client.(eth2client.ProposerDutiesProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide proposer duties")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
62
cmd/proposer/duties/process_internal_test.go
Normal file
62
cmd/proposer/duties/process_internal_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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 proposerduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"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")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "InvalidData",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "60s",
|
||||
"data": "[[",
|
||||
"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)
|
||||
}
|
||||
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/proposer/duties/run.go
Normal file
50
cmd/proposer/duties/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 proposerduties
|
||||
|
||||
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
|
||||
}
|
||||
61
cmd/proposerduties.go
Normal file
61
cmd/proposerduties.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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"
|
||||
proposerduties "github.com/wealdtech/ethdo/cmd/proposer/duties"
|
||||
)
|
||||
|
||||
var proposerDutiesCmd = &cobra.Command{
|
||||
Use: "duties",
|
||||
Short: "Obtain information about duties of an proposer",
|
||||
Long: `Obtain information about dutes of an proposer. For example:
|
||||
|
||||
ethdo proposer duties --epoch=12345
|
||||
|
||||
In quiet mode this will return 0 if duties can be obtained, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := proposerduties.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
proposerCmd.AddCommand(proposerDutiesCmd)
|
||||
proposerFlags(proposerDutiesCmd)
|
||||
proposerDutiesCmd.Flags().String("epoch", "", "the epoch for which to fetch duties")
|
||||
proposerDutiesCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
}
|
||||
|
||||
func proposerDutiesBindings() {
|
||||
if err := viper.BindPFlag("epoch", proposerDutiesCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", proposerDutiesCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||
return util.SetupStore()
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func includeCommandBindings(cmd *cobra.Command) {
|
||||
switch commandPath(cmd) {
|
||||
case "account/create":
|
||||
@@ -93,6 +94,8 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
blockAnalyzeBindings()
|
||||
case "block/info":
|
||||
blockInfoBindings()
|
||||
case "chain/eth1votes":
|
||||
chainEth1VotesBindings()
|
||||
case "chain/queues":
|
||||
chainQueuesBindings()
|
||||
case "chain/time":
|
||||
@@ -105,12 +108,16 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
exitVerifyBindings()
|
||||
case "node/events":
|
||||
nodeEventsBindings()
|
||||
case "proposer/duties":
|
||||
proposerDutiesBindings()
|
||||
case "slot/time":
|
||||
slotTimeBindings()
|
||||
case "synccommittee/inclusion":
|
||||
synccommitteeInclusionBindings()
|
||||
case "synccommittee/members":
|
||||
synccommitteeMembersBindings()
|
||||
case "validator/credentials/get":
|
||||
validatorCredentialsGetBindings()
|
||||
case "validator/depositdata":
|
||||
validatorDepositdataBindings()
|
||||
case "validator/duties":
|
||||
@@ -121,6 +128,8 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
validatorInfoBindings()
|
||||
case "validator/keycheck":
|
||||
validatorKeycheckBindings()
|
||||
case "validator/yield":
|
||||
validatorYieldBindings()
|
||||
case "validator/expectation":
|
||||
validatorExpectationBindings()
|
||||
case "wallet/create":
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"slot": "1",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ func newCommand(ctx context.Context) (*command, error) {
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
// Connection.
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
|
||||
@@ -37,14 +37,6 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"validators": "1",
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "NoValidator",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -32,6 +32,14 @@ func TestProcess(t *testing.T) {
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "MissingConnection",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"index": "1",
|
||||
},
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
{
|
||||
name: "InvalidConnection",
|
||||
vars: map[string]interface{}{
|
||||
@@ -39,7 +47,7 @@ func TestProcess(t *testing.T) {
|
||||
"index": "1",
|
||||
"connection": "invalid",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://invalid/eth/v1/beacon/genesis\": dial tcp: lookup invalid: no such host",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
|
||||
@@ -65,7 +65,15 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
{
|
||||
name: "ConnectionInvalid",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": "localhost:1",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
89
cmd/validator/credentials/get/command.go
Normal file
89
cmd/validator/credentials/get/command.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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 validatorcredentialsget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
|
||||
// Input.
|
||||
account string
|
||||
index string
|
||||
pubKey string
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Data access.
|
||||
consensusClient eth2client.Service
|
||||
validatorsProvider eth2client.ValidatorsProvider
|
||||
|
||||
// Output.
|
||||
validator *apiv1.Validator
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
c.account = viper.GetString("account")
|
||||
c.index = viper.GetString("index")
|
||||
c.pubKey = viper.GetString("pubkey")
|
||||
nonNil := 0
|
||||
if c.account != "" {
|
||||
nonNil++
|
||||
}
|
||||
if c.index != "" {
|
||||
nonNil++
|
||||
}
|
||||
if c.pubKey != "" {
|
||||
nonNil++
|
||||
}
|
||||
if nonNil == 0 {
|
||||
return nil, errors.New("one of account, index or pubkey required")
|
||||
}
|
||||
if nonNil > 1 {
|
||||
return nil, errors.New("only one of account, index and pubkey allowed")
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
91
cmd/validator/credentials/get/command_internal_test.go
Normal file
91
cmd/validator/credentials/get/command_internal_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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 validatorcredentialsget
|
||||
|
||||
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",
|
||||
"index": "1",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "NoValidatorInfo",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "one of account, index or pubkey required",
|
||||
},
|
||||
{
|
||||
name: "MultipleValidatorInfo",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"index": "1",
|
||||
"pubkey": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
},
|
||||
err: "only one of account, index and pubkey allowed",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"index": "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
44
cmd/validator/credentials/get/output.go
Normal file
44
cmd/validator/credentials/get/output.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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 validatorcredentialsget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
switch c.validator.Validator.WithdrawalCredentials[0] {
|
||||
case 0:
|
||||
builder.WriteString("BLS credentials: ")
|
||||
builder.WriteString(fmt.Sprintf("%#x", c.validator.Validator.WithdrawalCredentials))
|
||||
case 1:
|
||||
builder.WriteString("Ethereum execution address: ")
|
||||
builder.WriteString(fmt.Sprintf("%#x", c.validator.Validator.WithdrawalCredentials[12:]))
|
||||
if c.verbose {
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("Withdrawal credentials: ")
|
||||
builder.WriteString(fmt.Sprintf("%#x", c.validator.Validator.WithdrawalCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
139
cmd/validator/credentials/get/process.go
Normal file
139
cmd/validator/credentials/get/process.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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 validatorcredentialsget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"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
|
||||
}
|
||||
|
||||
// Work out which validator we are dealing with.
|
||||
if err := c.fetchValidator(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
data, err := json.Marshal(c.validator)
|
||||
if err == nil {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) setup(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// Connect to the consensus node.
|
||||
c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to consensus node")
|
||||
}
|
||||
|
||||
// Obtain the validators provider.
|
||||
var isProvider bool
|
||||
c.validatorsProvider, isProvider = c.consensusClient.(eth2client.ValidatorsProvider)
|
||||
if !isProvider {
|
||||
return errors.New("consensu node does not provide validator information")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) fetchValidator(ctx context.Context) error {
|
||||
if c.account != "" {
|
||||
_, account, err := util.WalletAndAccountFromInput(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to obtain account")
|
||||
}
|
||||
|
||||
accPubKey, err := util.BestPublicKey(account)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to obtain public key for account")
|
||||
}
|
||||
pubKey := phase0.BLSPubKey{}
|
||||
copy(pubKey[:], accPubKey.Marshal())
|
||||
validators, err := c.validatorsProvider.ValidatorsByPubKey(ctx,
|
||||
"head",
|
||||
[]phase0.BLSPubKey{pubKey},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
if len(validators) == 0 {
|
||||
return errors.New("unknown validator")
|
||||
}
|
||||
for _, validator := range validators {
|
||||
c.validator = validator
|
||||
}
|
||||
}
|
||||
if c.index != "" {
|
||||
tmp, err := strconv.ParseUint(c.index, 10, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid validator index")
|
||||
}
|
||||
index := phase0.ValidatorIndex(tmp)
|
||||
validators, err := c.validatorsProvider.Validators(ctx,
|
||||
"head",
|
||||
[]phase0.ValidatorIndex{index},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
if _, exists := validators[index]; !exists {
|
||||
return errors.New("unknown validator")
|
||||
}
|
||||
c.validator = validators[index]
|
||||
}
|
||||
if c.pubKey != "" {
|
||||
bytes, err := hex.DecodeString(strings.TrimPrefix(c.pubKey, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid validator public key")
|
||||
}
|
||||
pubKey := phase0.BLSPubKey{}
|
||||
copy(pubKey[:], bytes)
|
||||
|
||||
validators, err := c.validatorsProvider.ValidatorsByPubKey(ctx,
|
||||
"head",
|
||||
[]phase0.BLSPubKey{pubKey},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
if len(validators) == 0 {
|
||||
return errors.New("unknown validator")
|
||||
}
|
||||
for _, validator := range validators {
|
||||
c.validator = validator
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
50
cmd/validator/credentials/get/run.go
Normal file
50
cmd/validator/credentials/get/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 validatorcredentialsget
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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,8 @@ 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",
|
||||
[4]byte{0x90, 0x00, 0x00, 0x69}: "sepolia",
|
||||
}
|
||||
|
||||
if datum.validatorPubKey == nil {
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
|
||||
},
|
||||
{
|
||||
name: "EpochProvided",
|
||||
|
||||
84
cmd/validator/yield/command.go
Normal file
84
cmd/validator/yield/command.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 validatoryield
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
json bool
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Input.
|
||||
validators string
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
|
||||
// Output.
|
||||
results *output
|
||||
}
|
||||
|
||||
type output struct {
|
||||
BaseReward decimal.Decimal `json:"base_reward"`
|
||||
ActiveValidators decimal.Decimal `json:"active_validators"`
|
||||
ActiveValidatorBalance decimal.Decimal `json:"active_validator_balance"`
|
||||
ValidatorRewardsPerEpoch decimal.Decimal `json:"validator_rewards_per_epoch"`
|
||||
ValidatorRewardsPerYear decimal.Decimal `json:"validator_rewards_per_year"`
|
||||
ValidatorRewardsAllCorrect decimal.Decimal `json:"validator_rewards_all_correct"`
|
||||
ExpectedValidatorRewardsPerEpoch decimal.Decimal `json:"expected_validator_rewards_per_epoch"`
|
||||
MaxIssuancePerEpoch decimal.Decimal `json:"max_issuance_per_epoch"`
|
||||
MaxIssuancePerYear decimal.Decimal `json:"max_issuance_per_year"`
|
||||
Yield decimal.Decimal `json:"yield"`
|
||||
}
|
||||
|
||||
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"),
|
||||
results: &output{},
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
c.validators = viper.GetString("validators")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
73
cmd/validator/yield/command_internal_test.go
Normal file
73
cmd/validator/yield/command_internal_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validatoryield
|
||||
|
||||
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{}{
|
||||
"validators": "1",
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"validators": "1",
|
||||
"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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
67
cmd/validator/yield/output.go
Normal file
67
cmd/validator/yield/output.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validatoryield
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/wealdtech/go-string2eth"
|
||||
)
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.json {
|
||||
data, err := json.Marshal(c.results)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
if c.verbose {
|
||||
builder.WriteString("Per-validator rewards per epoch: ")
|
||||
builder.WriteString(string2eth.WeiToGWeiString(c.results.ValidatorRewardsPerEpoch.BigInt()))
|
||||
builder.WriteString("\n")
|
||||
|
||||
builder.WriteString("Per-validator rewards per year: ")
|
||||
builder.WriteString(string2eth.WeiToString(c.results.ValidatorRewardsPerYear.BigInt(), true))
|
||||
builder.WriteString("\n")
|
||||
|
||||
builder.WriteString("Expected per-validator rewards per epoch (with full participation): ")
|
||||
builder.WriteString(string2eth.WeiToGWeiString(c.results.ExpectedValidatorRewardsPerEpoch.BigInt()))
|
||||
builder.WriteString("\n")
|
||||
|
||||
builder.WriteString("Maximum chain issuance per epoch: ")
|
||||
builder.WriteString(string2eth.WeiToString(c.results.MaxIssuancePerEpoch.BigInt(), true))
|
||||
builder.WriteString("\n")
|
||||
|
||||
builder.WriteString("Maximum chain issuance per year: ")
|
||||
builder.WriteString(string2eth.WeiToString(c.results.MaxIssuancePerYear.BigInt(), true))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
builder.WriteString("Yield: ")
|
||||
builder.WriteString(c.results.Yield.Mul(decimal.New(100, 0)).StringFixed(2))
|
||||
builder.WriteString("%\n")
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
170
cmd/validator/yield/process.go
Normal file
170
cmd/validator/yield/process.go
Normal file
@@ -0,0 +1,170 @@
|
||||
// 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 validatoryield
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/shopspring/decimal"
|
||||
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
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Printf("Active validators: %v\n", c.results.ActiveValidators)
|
||||
fmt.Printf("Active validator balance: %v\n", c.results.ActiveValidatorBalance)
|
||||
}
|
||||
|
||||
return c.calculateYield(ctx)
|
||||
}
|
||||
|
||||
var weiPerGwei = decimal.New(1e9, 0)
|
||||
var one = decimal.New(1, 0)
|
||||
var epochsPerYear = decimal.New(225*365, 0)
|
||||
|
||||
// calculateYield calculates yield from the number of active validators.
|
||||
func (c *command) calculateYield(ctx context.Context) error {
|
||||
|
||||
spec, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, exists := spec["BASE_REWARD_FACTOR"]
|
||||
if !exists {
|
||||
return errors.New("spec missing BASE_REWARD_FACTOR")
|
||||
}
|
||||
baseReward, isType := tmp.(uint64)
|
||||
if !isType {
|
||||
return errors.New("BASE_REWARD_FACTOR of incorrect type")
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Printf("Base reward: %v\n", baseReward)
|
||||
}
|
||||
c.results.BaseReward = decimal.New(int64(baseReward), 0)
|
||||
|
||||
numerator := decimal.New(32, 0).Mul(weiPerGwei).Mul(c.results.BaseReward)
|
||||
if c.debug {
|
||||
fmt.Printf("Numerator: %v\n", numerator)
|
||||
}
|
||||
activeValidatorsBalanceInGwei := c.results.ActiveValidatorBalance.Div(weiPerGwei)
|
||||
denominator := decimal.NewFromBigInt(new(big.Int).Sqrt(activeValidatorsBalanceInGwei.BigInt()), 0)
|
||||
if c.debug {
|
||||
fmt.Printf("Denominator: %v\n", denominator)
|
||||
}
|
||||
c.results.ValidatorRewardsPerEpoch = numerator.Div(denominator).RoundDown(0).Mul(weiPerGwei)
|
||||
if c.debug {
|
||||
fmt.Printf("Validator rewards per epoch: %v\n", c.results.ValidatorRewardsPerEpoch)
|
||||
}
|
||||
c.results.ValidatorRewardsPerYear = c.results.ValidatorRewardsPerEpoch.Mul(epochsPerYear)
|
||||
if c.debug {
|
||||
fmt.Printf("Validator rewards per year: %v\n", c.results.ValidatorRewardsPerYear)
|
||||
}
|
||||
// Expected validator rewards assume that there is no proposal and no sync committee participation,
|
||||
// but that head/source/target are correct and timely: this gives 54/64 of the reward.
|
||||
// These values are obtained from https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#incentivization-weights
|
||||
c.results.ExpectedValidatorRewardsPerEpoch = c.results.ValidatorRewardsPerEpoch.Mul(decimal.New(54, 0)).Div(decimal.New(64, 0)).Div(weiPerGwei).RoundDown(0).Mul(weiPerGwei)
|
||||
if c.debug {
|
||||
fmt.Printf("Expected validator rewards per epoch: %v\n", c.results.ExpectedValidatorRewardsPerEpoch)
|
||||
}
|
||||
|
||||
c.results.MaxIssuancePerEpoch = c.results.ValidatorRewardsPerEpoch.Mul(c.results.ActiveValidators)
|
||||
if c.debug {
|
||||
fmt.Printf("Chain rewards per epoch: %v\n", c.results.MaxIssuancePerEpoch)
|
||||
}
|
||||
c.results.MaxIssuancePerYear = c.results.MaxIssuancePerEpoch.Mul(epochsPerYear)
|
||||
if c.debug {
|
||||
fmt.Printf("Chain rewards per year: %v\n", c.results.MaxIssuancePerYear)
|
||||
}
|
||||
|
||||
c.results.Yield = c.results.ValidatorRewardsPerYear.Div(weiPerGwei).Div(weiPerGwei).Div(decimal.New(32, 0))
|
||||
if c.debug {
|
||||
fmt.Printf("Yield: %v\n", c.results.Yield)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if c.validators == "" {
|
||||
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")
|
||||
}
|
||||
|
||||
// Obtain the number of active validators.
|
||||
var isProvider bool
|
||||
validatorsProvider, isProvider := c.eth2Client.(eth2client.ValidatorsProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide validator information")
|
||||
}
|
||||
|
||||
validators, err := validatorsProvider.Validators(ctx, "head", nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validators")
|
||||
}
|
||||
|
||||
currentEpoch := chainTime.CurrentEpoch()
|
||||
activeValidators := decimal.Zero
|
||||
activeValidatorBalance := decimal.Zero
|
||||
for _, validator := range validators {
|
||||
if validator.Validator.ActivationEpoch <= currentEpoch &&
|
||||
validator.Validator.ExitEpoch > currentEpoch {
|
||||
activeValidators = activeValidators.Add(one)
|
||||
activeValidatorBalance = activeValidatorBalance.Add(decimal.NewFromInt(int64(validator.Validator.EffectiveBalance)))
|
||||
}
|
||||
}
|
||||
c.results.ActiveValidators = activeValidators
|
||||
c.results.ActiveValidatorBalance = activeValidatorBalance.Mul(weiPerGwei)
|
||||
} else {
|
||||
activeValidators, err := strconv.ParseInt(c.validators, 0, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse number of validators")
|
||||
}
|
||||
if activeValidators <= 0 {
|
||||
return errors.New("number of validators must be greater than 0")
|
||||
}
|
||||
|
||||
c.results.ActiveValidators = decimal.New(activeValidators, 0)
|
||||
c.results.ActiveValidatorBalance = decimal.New(32, 0).Mul(c.results.ActiveValidators).Mul(weiPerGwei).Mul(weiPerGwei)
|
||||
if c.debug {
|
||||
fmt.Println("Assuming 32Ξ per validator")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
90
cmd/validator/yield/process_internal_test.go
Normal file
90
cmd/validator/yield/process_internal_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validatoryield
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"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")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "InvalidData",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "60s",
|
||||
"validators": "1",
|
||||
"data": "[[",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ValidatorsInvalid",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "60s",
|
||||
"validators": "invalid",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "failed to parse number of validators: strconv.ParseInt: parsing \"invalid\": invalid syntax",
|
||||
},
|
||||
{
|
||||
name: "ValidatorsNegative",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "60s",
|
||||
"validators": "-1",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "number of validators must be greater than 0",
|
||||
},
|
||||
{
|
||||
name: "ValidatorsZero",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "60s",
|
||||
"validators": "0",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "number of validators must be greater than 0",
|
||||
},
|
||||
}
|
||||
|
||||
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/validator/yield/run.go
Normal file
50
cmd/validator/yield/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validatoryield
|
||||
|
||||
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
|
||||
}
|
||||
32
cmd/validatorcredentials.go
Normal file
32
cmd/validatorcredentials.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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 (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// validatorCredentialsCmd represents the validator credentials command
|
||||
var validatorCredentialsCmd = &cobra.Command{
|
||||
Use: "credentials",
|
||||
Short: "Manage Ethereum consensus validator credentials",
|
||||
Long: `Manage Ethereum consensus validator credentials.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
validatorCmd.AddCommand(validatorCredentialsCmd)
|
||||
}
|
||||
|
||||
func validatorCredentialsFlags(cmd *cobra.Command) {
|
||||
}
|
||||
65
cmd/validatorcredentialsget.go
Normal file
65
cmd/validatorcredentialsget.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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"
|
||||
validatorcredentialsget "github.com/wealdtech/ethdo/cmd/validator/credentials/get"
|
||||
)
|
||||
|
||||
var validatorCredentialsGetCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Obtain withdrawal credentials for an Ethereum consensus validator",
|
||||
Long: `Obtain withdrawal credentials for an Ethereum consensus validator. For example:
|
||||
|
||||
ethdo validator credentials get --account=primary/validator
|
||||
|
||||
In quiet mode this will return 0 if the validator exists, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := validatorcredentialsget.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
validatorCredentialsCmd.AddCommand(validatorCredentialsGetCmd)
|
||||
validatorCredentialsFlags(validatorCredentialsGetCmd)
|
||||
validatorCredentialsGetCmd.Flags().String("account", "", "Account for which to fetch validator credentials")
|
||||
validatorCredentialsGetCmd.Flags().String("index", "", "Validator index for which to fetch validator credentials")
|
||||
validatorCredentialsGetCmd.Flags().String("pubkey", "", "Validator public key for which to fetch validator credentials")
|
||||
}
|
||||
|
||||
func validatorCredentialsGetBindings() {
|
||||
if err := viper.BindPFlag("account", validatorCredentialsGetCmd.Flags().Lookup("account")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("index", validatorCredentialsGetCmd.Flags().Lookup("index")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("pubkey", validatorCredentialsGetCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ In quiet mode this will return 0 if the validator information can be obtained, o
|
||||
pubKey, err := bestPublicKey(account)
|
||||
if err == nil {
|
||||
deposits, totalDeposited, err := graphData(network, pubKey.Marshal())
|
||||
if err == nil {
|
||||
if err == nil && deposits > 0 {
|
||||
fmt.Printf("Number of deposits: %d\n", deposits)
|
||||
fmt.Printf("Total deposited: %s\n", string2eth.GWeiToString(uint64(totalDeposited), true))
|
||||
}
|
||||
@@ -91,9 +91,14 @@ In quiet mode this will return 0 if the validator information can be obtained, o
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if validator.Status.IsPending() || validator.Status.HasActivated() {
|
||||
fmt.Printf("Index: %d\n", validator.Index)
|
||||
}
|
||||
if verbose {
|
||||
if validator.Status.IsPending() {
|
||||
fmt.Printf("Activation eligibility epoch: %d\n", validator.Validator.ActivationEligibilityEpoch)
|
||||
}
|
||||
if validator.Status.HasActivated() {
|
||||
fmt.Printf("Index: %d\n", validator.Index)
|
||||
fmt.Printf("Activation epoch: %d\n", validator.Validator.ActivationEpoch)
|
||||
}
|
||||
fmt.Printf("Public key: %#x\n", validator.Validator.PublicKey)
|
||||
|
||||
61
cmd/validatoryield.go
Normal file
61
cmd/validatoryield.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
validatoryield "github.com/wealdtech/ethdo/cmd/validator/yield"
|
||||
)
|
||||
|
||||
var validatorYieldCmd = &cobra.Command{
|
||||
Use: "yield",
|
||||
Short: "Calculate yield for validators",
|
||||
Long: `Calculate yield for validators. For example:
|
||||
|
||||
ethdo validator yield
|
||||
|
||||
It is important to understand the yield is both probabilistic and dependent on network conditions.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := validatoryield.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
res = strings.TrimRight(res, "\n")
|
||||
fmt.Println(res)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
validatorCmd.AddCommand(validatorYieldCmd)
|
||||
validatorFlags(validatorYieldCmd)
|
||||
validatorYieldCmd.Flags().String("validators", "", "Number of active validators (default fetches from chain)")
|
||||
validatorYieldCmd.Flags().Bool("json", false, "JSON output")
|
||||
}
|
||||
|
||||
func validatorYieldBindings() {
|
||||
if err := viper.BindPFlag("validators", validatorYieldCmd.Flags().Lookup("validators")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", validatorYieldCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -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.20.0)"
|
||||
var ReleaseVersion = "local build (latest release 1.25.0)"
|
||||
|
||||
// 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.
|
||||
@@ -437,23 +453,25 @@ Epoch commands focus on information about a beacon chain epoch.
|
||||
|
||||
```sh
|
||||
$ ethdo epoch summary
|
||||
Epoch 1406:
|
||||
Slot 44992 (0/32):
|
||||
Proposer: 31501
|
||||
Proposed: ✓
|
||||
Slot 44993 (1/32):
|
||||
Proposer: 9302
|
||||
Proposed: ✓
|
||||
...
|
||||
Sync committee validator 71248:
|
||||
Chances: 29
|
||||
Included: 7
|
||||
Inclusion %: 24.14
|
||||
Sync committee validator 87371:
|
||||
Chances: 29
|
||||
Included: 0
|
||||
Inclusion %: 0.00
|
||||
...
|
||||
Epoch 380:
|
||||
Proposals: 31/32 (96.88%)
|
||||
Attestations: 1530/1572 (97.33%)
|
||||
Sync committees: 13086/15872 (82.45%)
|
||||
```
|
||||
|
||||
More detailed information can be obtained with the `--verbose` flag:
|
||||
|
||||
```sh
|
||||
$ ethdo epoch summary --verbose
|
||||
Epoch 380:
|
||||
Proposals: 31/32 (96.88%)
|
||||
Slot 12188 (28/32) validator 1518 not proposed or not included
|
||||
Attestations: 1530/1572 (97.33%)
|
||||
Slot 12160 committee 0 validator 292 failed to participate
|
||||
Slot 12162 committee 0 validator 204 failed to participate
|
||||
Slot 12163 committee 0 validator 297 failed to participate
|
||||
Slot 12164 committee 0 validator 209 failed to participate
|
||||
...
|
||||
```
|
||||
|
||||
### `exit` comands
|
||||
@@ -560,14 +578,26 @@ $ ethdo synccommittee members
|
||||
|
||||
Validator commands focus on interaction with Ethereum 2 validators.
|
||||
|
||||
#### `credentials get`
|
||||
|
||||
`ethdo validator credentials get` provides information about the withdrawal credentials for the provided validator. Options include:
|
||||
- `account` the account for which to obtain the withdrawal credentials (in format "wallet/account")
|
||||
- `pubkey` the public key of the validator for which to obtain the withdrawal credentials
|
||||
- `index` the index of the validator for which to obtain the withdrawal credentials
|
||||
|
||||
```sh
|
||||
$ ethdo validator credentials get --account=Validators/1
|
||||
```
|
||||
|
||||
#### `depositdata`
|
||||
|
||||
`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`
|
||||
@@ -670,6 +700,37 @@ $ ethdo attester inclusion --account=Validators/1 --epoch=6484
|
||||
Attestation included in block 207492 (inclusion delay 1)
|
||||
```
|
||||
|
||||
#### `yield`
|
||||
|
||||
`ethdo validator yield` calculates the expected yield given the number of validators. Options include:
|
||||
- `validators` use a specified number of validators rather than the current number of active validators
|
||||
- `json` obtain detailed information in JSON format
|
||||
|
||||
```sh
|
||||
$ ethdo validator yield
|
||||
Yield: 4.64%
|
||||
```
|
||||
|
||||
### `proposer` commands
|
||||
|
||||
Proposer commands focus on Ethereum 2 validators' actions as proposers.
|
||||
|
||||
#### `duties`
|
||||
|
||||
`ethdo proposer duties` provides information on the proposal duties for a given epoch. Options include:
|
||||
- `epoch` the epoch in which to obtain the duties (defaults to current epoch)
|
||||
- `json` obtain detailed information in JSON format
|
||||
|
||||
```sh
|
||||
$ ethdo proposer duties --epoch=5
|
||||
Epoch 5:
|
||||
Slot 160: validator 8221
|
||||
Slot 161: validator 11193
|
||||
Slot 162: validator 4116
|
||||
Slot 163: validator 631
|
||||
...
|
||||
```
|
||||
|
||||
## Maintainers
|
||||
|
||||
Jim McDonald: [@mcdee](https://github.com/mcdee).
|
||||
|
||||
3
go.mod
3
go.mod
@@ -22,6 +22,7 @@ require (
|
||||
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7
|
||||
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/spf13/afero v1.8.0 // indirect
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
@@ -42,7 +43,7 @@ require (
|
||||
github.com/wealdtech/go-eth2-wallet-store-s3 v1.10.0
|
||||
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.0
|
||||
github.com/wealdtech/go-eth2-wallet-types/v2 v2.9.0
|
||||
github.com/wealdtech/go-string2eth v1.1.0
|
||||
github.com/wealdtech/go-string2eth v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed // indirect
|
||||
golang.org/x/text v0.3.7
|
||||
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 // indirect
|
||||
|
||||
9
go.sum
9
go.sum
@@ -75,8 +75,6 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
|
||||
github.com/attestantio/dirk v1.1.0 h1:hwMTYZkwj/Y0um3OD0LQxg2xSl4/5xqVWV2MRePE4ec=
|
||||
github.com/attestantio/dirk v1.1.0/go.mod h1:2jkOw/XHjvIDdhDcmj+Z3kuVPpxMcQ6zxzzjSSv71PY=
|
||||
github.com/attestantio/go-eth2-client v0.8.1/go.mod h1:kEK9iAAOBoADO5wEkd84FEOzjT1zXgVWveQsqn+uBGg=
|
||||
github.com/attestantio/go-eth2-client v0.10.0 h1:nmOmzErfz4I2gEkucHKOaFwkbwD4i6JbIX38Z8Dm4Tc=
|
||||
github.com/attestantio/go-eth2-client v0.10.0/go.mod h1:ijuXoXJCBFMexUYaBOl8PXfZKwYUFJy7cV03TMdw8Bo=
|
||||
github.com/attestantio/go-eth2-client v0.11.0 h1:8/Jn5AAfd+4tOggLi+FvOv9/ORaObECv42ab7vK2FJc=
|
||||
github.com/attestantio/go-eth2-client v0.11.0/go.mod h1:zXL/BxC0cBBhxj+tP7QG7t9Ufoa8GwQLdlbvZRd9+dM=
|
||||
github.com/aws/aws-sdk-go v1.33.17/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
@@ -354,7 +352,6 @@ github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQ
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.11 h1:i2lw1Pm7Yi/4O6XCSyJWqEHI2MDw2FzUK6o/D21xn2A=
|
||||
github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
@@ -495,6 +492,8 @@ github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDN
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
@@ -589,6 +588,8 @@ github.com/wealdtech/go-indexer v1.0.0/go.mod h1:u1cjsbsOXsm5jzJDyLmZY7GsrdX8KYX
|
||||
github.com/wealdtech/go-majordomo v1.0.1/go.mod h1:QoT4S1nUQwdQK19+CfepDwV+Yr7cc3dbF+6JFdQnIqY=
|
||||
github.com/wealdtech/go-string2eth v1.1.0 h1:USJQmysUrBYYmZs7d45pMb90hRSyEwizP7lZaOZLDAw=
|
||||
github.com/wealdtech/go-string2eth v1.1.0/go.mod h1:RUzsLjJtbZaJ/3UKn9kY19a/vCCUHtEWoUW3uiK6yGU=
|
||||
github.com/wealdtech/go-string2eth v1.2.0 h1:C0E5p78tecZTsGccJc9r/kreFah4EfDs5uUPnS6XXMs=
|
||||
github.com/wealdtech/go-string2eth v1.2.0/go.mod h1:RUzsLjJtbZaJ/3UKn9kY19a/vCCUHtEWoUW3uiK6yGU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -843,8 +844,6 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// Copyright © 2020, 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
|
||||
|
||||
@@ -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