Compare commits

...

10 Commits

Author SHA1 Message Date
Jim McDonald
d60d8beb0b Bump version. 2022-03-11 14:16:49 +00:00
Jim McDonald
a657b3bc24 Do not show execution payload if empty. 2022-03-11 14:16:09 +00:00
Jim McDonald
28b90414d2 Bump version. 2022-03-11 13:39:35 +00:00
Jim McDonald
879a20a7af Support bellatrix. 2022-03-11 13:38:22 +00:00
Jim McDonald
3d49e091e5 Add block analyze. 2022-03-06 22:47:37 +00:00
Jim McDonald
3b51c67e7d Add ssz option to block info. 2022-02-16 15:06:17 +00:00
Jim McDonald
1d559c167b Update dependencies. 2022-01-31 08:10:44 +00:00
Jim McDonald
824c53f6f2 Do not write out text-based error messages whilst streaming with json output. 2022-01-24 16:59:01 +00:00
Jim McDonald
50ffdcd97c Allow custom epochs for attester duties. 2022-01-23 22:38:15 +00:00
Jim McDonald
0e79334863 Tidy up synccommittee inclusion output. 2022-01-17 22:06:08 +00:00
21 changed files with 1565 additions and 294 deletions

View File

@@ -1,3 +1,11 @@
1.18.1:
- do not show execution payload if empty
1.18.0:
- add "-ssz" option to "block info"
- add "block analyze" command
- support bellatrix
1.17.0:
- add sync committee information to "chain time"
- add details of vote success to "attester inclusion --verbose"

View File

@@ -66,14 +66,16 @@ func input(ctx context.Context) (*dataIn, error) {
return nil, err
}
// Required data.
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
// Epoch
epoch := viper.GetInt64("epoch")
if epoch == -1 {
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {

View File

@@ -0,0 +1,136 @@
// 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 blockanalyze
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
"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.
blockID string
stream bool
jsonOutput bool
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
blocksProvider eth2client.SignedBeaconBlockProvider
blockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Constants.
timelySourceWeight uint64
timelyTargetWeight uint64
timelyHeadWeight uint64
syncRewardWeight uint64
proposerWeight uint64
weightDenominator uint64
// Processing.
priorAttestations map[string]*attestationData
// Head roots provides the root of the head slot at given slots.
headRoots map[phase0.Slot]phase0.Root
// Target roots provides the root of the target epoch at given slots.
targetRoots map[phase0.Slot]phase0.Root
// Block info.
// Map is slot -> committee index -> validator committee index -> votes.
votes map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist
// Results.
analysis *blockAnalysis
}
type blockAnalysis struct {
Slot phase0.Slot `json:"slot"`
Attestations []*attestationAnalysis `json:"attestations"`
SyncCommitee *syncCommitteeAnalysis `json:"sync_committee"`
Value float64 `json:"value"`
}
type attestationAnalysis struct {
Head phase0.Root `json:"head"`
Target phase0.Root `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
type syncCommitteeAnalysis struct {
Contributions int `json:"contributions"`
PossibleContributions int `json:"possible_contributions"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
type attestationData struct {
Block phase0.Slot `json:"block"`
Index int `json:"index"`
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
priorAttestations: make(map[string]*attestationData),
headRoots: make(map[phase0.Slot]phase0.Root),
targetRoots: make(map[phase0.Slot]phase0.Root),
votes: make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist),
}
// 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.blockID = viper.GetString("blockid")
c.stream = viper.GetBool("stream")
c.jsonOutput = viper.GetBool("json")
return c, nil
}

View File

@@ -0,0 +1,82 @@
// 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 blockanalyze
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: "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",
"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)
}
})
}
}

157
cmd/block/analyze/output.go Normal file
View File

@@ -0,0 +1,157 @@
// 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 blockanalyze
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)
}
type attestationAnalysisJSON struct {
Head string `json:"head"`
Target string `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
func (a *attestationAnalysis) MarshalJSON() ([]byte, error) {
return json.Marshal(attestationAnalysisJSON{
Head: fmt.Sprintf("%#x", a.Head),
Target: fmt.Sprintf("%#x", a.Target),
Distance: a.Distance,
Duplicate: a.Duplicate,
NewVotes: a.NewVotes,
Votes: a.Votes,
PossibleVotes: a.PossibleVotes,
HeadCorrect: a.HeadCorrect,
HeadTimely: a.HeadTimely,
SourceTimely: a.SourceTimely,
TargetCorrect: a.TargetCorrect,
TargetTimely: a.TargetTimely,
Score: a.Score,
Value: a.Value,
})
}
func (c *command) outputJSON(_ context.Context) (string, error) {
data, err := json.Marshal(c.analysis)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputTxt(_ context.Context) (string, error) {
builder := strings.Builder{}
for i, attestation := range c.analysis.Attestations {
if c.verbose {
builder.WriteString("Attestation ")
builder.WriteString(fmt.Sprintf("%d", i))
builder.WriteString(": ")
builder.WriteString("distance ")
builder.WriteString(fmt.Sprintf("%d", attestation.Distance))
builder.WriteString(", ")
if attestation.Duplicate != nil {
builder.WriteString("duplicate of attestation ")
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Index))
builder.WriteString(" in block ")
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Block))
builder.WriteString("\n")
continue
}
builder.WriteString(fmt.Sprintf("%d", attestation.NewVotes))
builder.WriteString("/")
builder.WriteString(fmt.Sprintf("%d", attestation.Votes))
builder.WriteString("/")
builder.WriteString(fmt.Sprintf("%d", attestation.PossibleVotes))
builder.WriteString(" new/total/possible votes")
if attestation.NewVotes == 0 {
builder.WriteString("\n")
continue
} else {
builder.WriteString(", ")
}
switch {
case !attestation.HeadCorrect:
builder.WriteString("head vote incorrect, ")
case !attestation.HeadTimely:
builder.WriteString("head vote correct but late, ")
}
if !attestation.SourceTimely {
builder.WriteString("source vote late, ")
}
switch {
case !attestation.TargetCorrect:
builder.WriteString("target vote incorrect, ")
case !attestation.TargetTimely:
builder.WriteString("target vote correct but late, ")
}
builder.WriteString("score ")
builder.WriteString(fmt.Sprintf("%0.3f", attestation.Score))
builder.WriteString(", value ")
builder.WriteString(fmt.Sprintf("%0.3f", attestation.Value))
builder.WriteString("\n")
}
}
if c.analysis.SyncCommitee.Contributions > 0 {
if c.verbose {
builder.WriteString("Sync committee contributions: ")
builder.WriteString(fmt.Sprintf("%d", c.analysis.SyncCommitee.Contributions))
builder.WriteString(" contributions, score ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Score))
builder.WriteString(", value ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Value))
builder.WriteString("\n")
}
}
builder.WriteString("Value for block ")
builder.WriteString(fmt.Sprintf("%d", c.analysis.Slot))
builder.WriteString(": ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.Value))
builder.WriteString("\n")
return builder.String(), nil
}

View File

@@ -0,0 +1,431 @@
// 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 blockanalyze
import (
"bytes"
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
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
}
block, err := c.blocksProvider.SignedBeaconBlock(ctx, c.blockID)
if err != nil {
return errors.Wrap(err, "failed to obtain beacon block")
}
if block == nil {
return errors.New("empty beacon block")
}
slot, err := block.Slot()
if err != nil {
return err
}
attestations, err := block.Attestations()
if err != nil {
return err
}
c.analysis = &blockAnalysis{
Slot: slot,
}
// Calculate how many parents we need to fetch.
minSlot := slot
for _, attestation := range attestations {
if attestation.Data.Slot < minSlot {
minSlot = attestation.Data.Slot
}
}
if c.debug {
fmt.Printf("Need to fetch blocks to slot %d\n", minSlot)
}
if err := c.fetchParents(ctx, block, minSlot); err != nil {
return err
}
return c.analyze(ctx, block)
}
func (c *command) analyze(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
if err := c.analyzeAttestations(ctx, block); err != nil {
return err
}
if err := c.analyzeSyncCommittees(ctx, block); err != nil {
return err
}
return nil
}
func (c *command) analyzeAttestations(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
}
slot, err := block.Slot()
if err != nil {
return err
}
c.analysis.Attestations = make([]*attestationAnalysis, len(attestations))
blockVotes := make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist)
for i, attestation := range attestations {
if c.debug {
fmt.Printf("Processing attestation %d\n", i)
}
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
root, err := attestation.HashTreeRoot()
if err != nil {
return err
}
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
var err error
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate head timely.
analysis.HeadTimely = attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate target timely.
analysis.TargetTimely = attestation.Data.Slot >= slot-32
}
// Calculate score and value.
if analysis.TargetCorrect && analysis.TargetTimely {
analysis.Score += float64(c.timelyTargetWeight) / float64(c.weightDenominator)
}
if analysis.SourceTimely {
analysis.Score += float64(c.timelySourceWeight) / float64(c.weightDenominator)
}
if analysis.HeadCorrect && analysis.HeadTimely {
analysis.Score += float64(c.timelyHeadWeight) / float64(c.weightDenominator)
}
analysis.Value = analysis.Score * float64(analysis.NewVotes)
c.analysis.Value += analysis.Value
c.analysis.Attestations[i] = analysis
}
return nil
}
func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedBeaconBlock, minSlot phase0.Slot) error {
parentRoot, err := block.ParentRoot()
if err != nil {
return err
}
// Obtain the parent block.
parentBlock, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", parentRoot))
if err != nil {
return err
}
if parentBlock == nil {
return fmt.Errorf("unable to obtain parent block %s", parentBlock)
}
parentSlot, err := parentBlock.Slot()
if err != nil {
return err
}
if parentSlot < minSlot {
return nil
}
if err := c.processParentBlock(ctx, parentBlock); err != nil {
return err
}
return c.fetchParents(ctx, parentBlock, minSlot)
}
func (c *command) processParentBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
}
slot, err := block.Slot()
if err != nil {
return err
}
if c.debug {
fmt.Printf("Processing block %d\n", slot)
}
for i, attestation := range attestations {
root, err := attestation.HashTreeRoot()
if err != nil {
return err
}
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
Block: slot,
Index: i,
}
data := attestation.Data
_, exists := c.votes[data.Slot]
if !exists {
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = c.votes[data.Slot][data.Index]
if !exists {
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
c.votes[data.Slot][data.Index].SetBitAt(j, true)
}
}
}
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")
}
// Obtain the number of active validators.
var isProvider bool
c.blocksProvider, isProvider = c.eth2Client.(eth2client.SignedBeaconBlockProvider)
if !isProvider {
return errors.New("connection does not provide signed beacon block information")
}
c.blockHeadersProvider, isProvider = c.eth2Client.(eth2client.BeaconBlockHeadersProvider)
if !isProvider {
return errors.New("connection does not provide beacon block header information")
}
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["TIMELY_SOURCE_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
}
var ok bool
c.timelySourceWeight, ok = tmp.(uint64)
if !ok {
return errors.New("TIMELY_SOURCE_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_TARGET_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(26)
}
c.timelyTargetWeight, ok = tmp.(uint64)
if !ok {
return errors.New("TIMELY_TARGET_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_HEAD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
}
c.timelyHeadWeight, ok = tmp.(uint64)
if !ok {
return errors.New("TIMELY_HEAD_WEIGHT of unexpected type")
}
tmp, exists = spec["SYNC_REWARD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(2)
}
c.syncRewardWeight, ok = tmp.(uint64)
if !ok {
return errors.New("SYNC_REWARD_WEIGHT of unexpected type")
}
tmp, exists = spec["PROPOSER_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(8)
}
c.proposerWeight, ok = tmp.(uint64)
if !ok {
return errors.New("PROPOSER_WEIGHT of unexpected type")
}
tmp, exists = spec["WEIGHT_DENOMINATOR"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(64)
}
c.weightDenominator, ok = tmp.(uint64)
if !ok {
return errors.New("WEIGHT_DENOMINATOR of unexpected type")
}
return nil
}
func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
root, exists := c.headRoots[slot]
if !exists {
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
c.headRoots[attestation.Data.Slot] = header.Root
root = header.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
root, exists := c.targetRoots[attestation.Data.Slot]
if !exists {
// Start with first slot of the target epoch.
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
c.targetRoots[attestation.Data.Slot] = header.Root
root = header.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
}
func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
c.analysis.SyncCommitee = &syncCommitteeAnalysis{}
switch block.Version {
case spec.DataVersionPhase0:
return nil
case spec.DataVersionAltair:
c.analysis.SyncCommitee.Contributions = int(block.Altair.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Altair.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
default:
return fmt.Errorf("unsupported block version %d", block.Version)
}
}

View File

@@ -0,0 +1,63 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockanalyze
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"),
},
},
}
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/block/analyze/run.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockanalyze
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
}

View File

@@ -32,6 +32,7 @@ type dataIn struct {
// Operation.
eth2Client eth2client.Service
jsonOutput bool
sszOutput bool
// Chain information.
blockID string
stream bool
@@ -48,6 +49,7 @@ func input(ctx context.Context) (*dataIn, error) {
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
data.jsonOutput = viper.GetBool("json")
data.sszOutput = viper.GetBool("ssz")
data.stream = viper.GetBool("stream")

View File

@@ -18,6 +18,7 @@ import (
"context"
"encoding/hex"
"fmt"
"math/big"
"sort"
"strings"
"time"
@@ -25,6 +26,7 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
@@ -267,6 +269,96 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
return res.String(), nil
}
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate body root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti,
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Sync aggregate.
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *altair.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
@@ -427,6 +519,59 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
return res.String(), nil
}
func outputBlockExecutionPayload(ctx context.Context,
verbose bool,
payload *bellatrix.ExecutionPayload,
) (
string,
error,
) {
if payload == nil {
return "", nil
}
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
if verbose {
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
for i := 0; i < 32; i++ {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
}
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
res.WriteString(" Base fee per gas: ")
res.WriteString(string2eth.WeiToString(baseFeePerGas, true))
res.WriteString("\n Block hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
res.WriteString(" Timestamp: ")
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
res.WriteString(" Prev RANDAO: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
res.WriteString(" Receipts root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
}
return res.String(), nil
}
// intersection returns a list of items common between the two sets.
func intersection(set1 []uint64, set2 []uint64) []phase0.ValidatorIndex {
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })

View File

@@ -23,11 +23,13 @@ import (
api "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/bellatrix"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
var jsonOutput bool
var sszOutput bool
var results *dataOut
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
@@ -66,7 +68,11 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionAltair:
if err := outputAltairBlock(ctx, data.jsonOutput, signedBlock.Altair); err != nil {
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Altair); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
@@ -75,6 +81,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data.stream {
jsonOutput = data.jsonOutput
sszOutput = data.sszOutput
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
if err != nil {
return nil, errors.Wrap(err, "failed to start block stream")
@@ -94,26 +101,43 @@ func headEventHandler(event *api.Event) {
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
if err != nil {
fmt.Printf("Failed to obtain block: %v\n", err)
if !jsonOutput {
fmt.Printf("Failed to obtain block: %v\n", err)
}
return
}
if signedBlock == nil {
fmt.Println("Empty beacon block")
if !jsonOutput {
fmt.Println("Empty beacon block")
}
return
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
fmt.Printf("Failed to output block: %v\n", err)
if !jsonOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
case spec.DataVersionAltair:
if err := outputAltairBlock(context.Background(), jsonOutput, signedBlock.Altair); err != nil {
fmt.Printf("Failed to output block: %v\n", err)
if err := outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair); err != nil {
if !jsonOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix); err != nil {
if !jsonOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
default:
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
if !jsonOutput {
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
}
return
}
}
@@ -136,7 +160,7 @@ func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0
return nil
}
func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair.SignedBeaconBlock) error {
func outputAltairBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *altair.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
@@ -144,6 +168,12 @@ func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
case sszOutput:
data, err := signedBlock.MarshalSSZ()
if err != nil {
return errors.Wrap(err, "failed to generate SSZ")
}
fmt.Printf("%x\n", data)
default:
data, err := outputAltairBlockText(ctx, results, signedBlock)
if err != nil {
@@ -153,3 +183,27 @@ func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair
}
return nil
}
func outputBellatrixBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *bellatrix.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
case sszOutput:
data, err := signedBlock.MarshalSSZ()
if err != nil {
return errors.Wrap(err, "failed to generate SSZ")
}
fmt.Printf("%x\n", data)
default:
data, err := outputBellatrixBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
}
return nil
}

65
cmd/blockanalyze.go Normal file
View 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"
blockanalyze "github.com/wealdtech/ethdo/cmd/block/analyze"
)
var blockAnalyzeCmd = &cobra.Command{
Use: "analyze",
Short: "Analyze a block",
Long: `Analyze the contents of a block. For example:
ethdo block analyze --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := blockanalyze.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Print(res)
}
return nil
},
}
func init() {
blockCmd.AddCommand(blockAnalyzeCmd)
blockFlags(blockAnalyzeCmd)
blockAnalyzeCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockAnalyzeCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockAnalyzeCmd.Flags().Bool("json", false, "output data in JSON format")
}
func blockAnalyzeBindings() {
if err := viper.BindPFlag("blockid", blockAnalyzeCmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockAnalyzeCmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockAnalyzeCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -26,7 +26,7 @@ var blockInfoCmd = &cobra.Command{
Short: "Obtain information about a block",
Long: `Obtain information about a block. For example:
ethdo block info --slot=12345
ethdo block info --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -50,6 +50,7 @@ func init() {
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
}
func blockInfoBindings() {
@@ -62,4 +63,7 @@ func blockInfoBindings() {
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
if err := viper.BindPFlag("ssz", blockInfoCmd.Flags().Lookup("ssz")); err != nil {
panic(err)
}
}

View File

@@ -89,6 +89,8 @@ func includeCommandBindings(cmd *cobra.Command) {
attesterDutiesBindings()
case "attester/inclusion":
attesterInclusionBindings()
case "block/analyze":
blockAnalyzeBindings()
case "block/info":
blockInfoBindings()
case "chain/time":

View File

@@ -26,17 +26,18 @@ func (c *command) output(ctx context.Context) (string, error) {
builder := strings.Builder{}
builder.WriteString("Epoch: ")
builder.WriteString(fmt.Sprintf("%d", c.epoch))
if c.verbose {
builder.WriteString("Epoch: ")
builder.WriteString(fmt.Sprintf("%d\n", c.epoch))
}
if !c.inCommittee {
builder.WriteString("\nValidator not in sync committee\n")
builder.WriteString("Validator not in sync committee")
} else {
if c.verbose {
builder.WriteString("Validator sync committee index ")
builder.WriteString(fmt.Sprintf("%d", c.committeeIndex))
builder.WriteString(fmt.Sprintf("%d\n", c.committeeIndex))
}
builder.WriteString("\n")
noBlock := 0
included := 0

View File

@@ -80,6 +80,13 @@ func (c *command) process(ctx context.Context) error {
} else {
c.inclusions = append(c.inclusions, 2)
}
case spec.DataVersionBellatrix:
aggregate = block.Bellatrix.Message.Body.SyncAggregate
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
c.inclusions = append(c.inclusions, 1)
} else {
c.inclusions = append(c.inclusions, 2)
}
default:
return fmt.Errorf("unhandled block version %v", block.Version)
}

View File

@@ -24,7 +24,7 @@ import (
// ReleaseVersion is the release version of the codebase.
// Usually overridden by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.17.0)"
var ReleaseVersion = "local build (latest release 1.18.1)"
// versionCmd represents the version command
var versionCmd = &cobra.Command{

View File

@@ -266,13 +266,35 @@ $ ethdo version
### `block` commands
Block commands focus on providing information about Ethereum 2 blocks.
#### `analyze`
`ethdo block info` obtains information about a block in Ethereum 2. Options include:
- `blockid`: the ID (slot, root, 'head') of the block to obtain
```sh
$ ethdo block analyze --blockid=80
Value for block 80: 488.531
```
Additional information is supplied when using `--verbose`
```
$ ethdo block analyze --blockid=80 --verbose
Attestation 0: distance 1, 119/119/132 new/total/possible votes, score 0.844, value 100.406
Attestation 1: distance 1, 116/116/131 new/total/possible votes, score 0.844, value 97.875
Attestation 2: distance 1, 115/115/131 new/total/possible votes, score 0.844, value 97.031
Attestation 3: distance 1, 114/114/132 new/total/possible votes, score 0.844, value 96.188
Attestation 4: distance 1, 113/113/132 new/total/possible votes, score 0.844, value 95.344
Attestation 5: distance 1, 2/22/132 new/total/possible votes, score 0.844, value 1.688
Value for block 80: 488.531
```
#### `info`
`ethdo block info` obtains information about a block in Ethereum 2. Options include:
- `slot`: the slot at which to attempt to fetch the block
- `blockid`: the ID (slot, root, 'head') of the block to obtain
```sh
$ ethdo block info --slot=80
$ ethdo block info --blockid=80
Attestations: 1
Attester slashings: 0
Deposits: 0

47
go.mod
View File

@@ -4,43 +4,48 @@ go 1.16
require (
github.com/OneOfOne/xxhash v1.2.5 // indirect
github.com/attestantio/dirk v1.0.4
github.com/attestantio/go-eth2-client v0.9.2
github.com/ferranbt/fastssz v0.0.0-20210905181407-59cf6761a7d5
github.com/gofrs/uuid v4.0.0+incompatible
github.com/attestantio/dirk v1.1.0
github.com/attestantio/go-eth2-client v0.11.0
github.com/aws/aws-sdk-go v1.42.44 // indirect
github.com/ferranbt/fastssz v0.0.0-20220103083642-bc5fefefa28b
github.com/gofrs/uuid v4.2.0+incompatible
github.com/google/uuid v1.3.0
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/hashicorp/hcl v1.0.1-vault-3 // indirect
github.com/herumi/bls-eth-go-binary v0.0.0-20210902234237-7763804ee078
github.com/herumi/bls-eth-go-binary v0.0.0-20220103074059-01b0ca9e9ef7
github.com/jackc/puddle v1.2.1 // indirect
github.com/minio/highwayhash v1.0.2 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pkg/errors v0.9.1
github.com/protolambda/zssz v0.1.5 // indirect
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.23.0
github.com/spf13/cobra v1.2.1
github.com/rs/zerolog v1.26.1
github.com/spf13/afero v1.8.0 // indirect
github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/tyler-smith/go-bip39 v1.1.0
github.com/wealdtech/go-bytesutil v1.1.1
github.com/wealdtech/go-ecodec v1.1.2
github.com/wealdtech/go-eth2-types/v2 v2.5.6
github.com/wealdtech/go-eth2-util v1.6.5
github.com/wealdtech/go-eth2-wallet v1.14.6
github.com/wealdtech/go-eth2-wallet-dirk v1.1.8
github.com/wealdtech/go-eth2-types/v2 v2.6.0
github.com/wealdtech/go-eth2-util v1.7.0
github.com/wealdtech/go-eth2-wallet v1.15.0
github.com/wealdtech/go-eth2-wallet-dirk v1.2.0
github.com/wealdtech/go-eth2-wallet-distributed v1.1.4
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.6
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.5.5
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.3.4
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.15
github.com/wealdtech/go-eth2-wallet-store-s3 v1.9.5
github.com/wealdtech/go-eth2-wallet-store-scratch v1.6.3
github.com/wealdtech/go-eth2-wallet-types/v2 v2.8.5
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.2.0
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.6.0
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.4.0
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.17.0
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
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed // indirect
golang.org/x/text v0.3.7
google.golang.org/grpc v1.40.0
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 // indirect
google.golang.org/grpc v1.44.0
gopkg.in/ini.v1 v1.66.3 // indirect
)

503
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,7 @@ type Service struct {
slotsPerEpoch uint64
epochsPerSyncCommitteePeriod uint64
altairForkEpoch phase0.Epoch
bellatrixForkEpoch phase0.Epoch
}
// module-wide log.
@@ -92,12 +93,20 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
}
log.Trace().Uint64("epoch", uint64(altairForkEpoch)).Msg("Obtained Altair fork epoch")
bellatrixForkEpoch, err := fetchBellatrixForkEpoch(ctx, parameters.forkScheduleProvider)
if err != nil {
// Set to far future epoch.
bellatrixForkEpoch = 0xffffffffffffffff
}
log.Trace().Uint64("epoch", uint64(bellatrixForkEpoch)).Msg("Obtained Bellatrix fork epoch")
s := &Service{
genesisTime: genesisTime,
slotDuration: slotDuration,
slotsPerEpoch: slotsPerEpoch,
epochsPerSyncCommitteePeriod: epochsPerSyncCommitteePeriod,
altairForkEpoch: altairForkEpoch,
bellatrixForkEpoch: bellatrixForkEpoch,
}
return s, nil
@@ -213,3 +222,28 @@ func fetchAltairForkEpoch(ctx context.Context, provider eth2client.ForkScheduleP
}
return 0, errors.New("no altair fork obtained")
}
// BellatrixInitialEpoch provides the epoch at which the Bellatrix hard fork takes place.
func (s *Service) BellatrixInitialEpoch() phase0.Epoch {
return s.bellatrixForkEpoch
}
func fetchBellatrixForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
forkSchedule, err := provider.ForkSchedule(ctx)
if err != nil {
return 0, err
}
count := 0
for i := range forkSchedule {
count++
if bytes.Equal(forkSchedule[i].CurrentVersion[:], forkSchedule[i].PreviousVersion[:]) {
// This is the genesis fork; ignore it.
continue
}
if count == 1 {
return forkSchedule[i].Epoch, nil
}
count++
}
return 0, errors.New("no bellatrix fork obtained")
}