mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-08 21:48:05 -05:00
Add block analyze.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
dev:
|
||||
- add "-ssz" option to "block info"
|
||||
- add "block analyze" command
|
||||
|
||||
1.17.0:
|
||||
- add sync committee information to "chain time"
|
||||
|
||||
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
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user