mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b90414d2 | ||
|
|
879a20a7af | ||
|
|
3d49e091e5 | ||
|
|
3b51c67e7d | ||
|
|
1d559c167b | ||
|
|
824c53f6f2 | ||
|
|
50ffdcd97c | ||
|
|
0e79334863 |
@@ -1,3 +1,8 @@
|
||||
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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
136
cmd/block/analyze/command.go
Normal file
136
cmd/block/analyze/command.go
Normal 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
|
||||
}
|
||||
82
cmd/block/analyze/command_internal_test.go
Normal file
82
cmd/block/analyze/command_internal_test.go
Normal 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
157
cmd/block/analyze/output.go
Normal 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
|
||||
}
|
||||
431
cmd/block/analyze/process.go
Normal file
431
cmd/block/analyze/process.go
Normal 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)
|
||||
}
|
||||
}
|
||||
63
cmd/block/analyze/process_internal_test.go
Normal file
63
cmd/block/analyze/process_internal_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright © 2022 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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
50
cmd/block/analyze/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 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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,56 @@ 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
|
||||
}
|
||||
|
||||
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] })
|
||||
|
||||
@@ -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
65
cmd/blockanalyze.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"
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.0)"
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
@@ -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
47
go.mod
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user