Compare commits

..

10 Commits

Author SHA1 Message Date
Jim McDonald
b89154ada3 Add "block trail". 2025-02-26 09:40:03 +00:00
Jim McDonald
b9fff0dbde Handle blocks without blobs. 2025-02-25 12:54:58 +00:00
Jim McDonald
0238130895 Fix incorrect argument binding. 2025-02-22 07:23:40 +00:00
Jim McDonald
60b6ce44f1 Merge pull request #159 from wealdtech/update_index
Handle electra attestations for epoch summary
2025-02-22 07:14:47 +00:00
Chris Berry
24486f0175 Merge pull request #160 from wealdtech/fix_help
Update help text to match args
2025-02-21 15:10:50 +00:00
Chris Berry
503bf9a996 Update help text to match args 2025-02-21 14:46:36 +00:00
Jim McDonald
1d14a57204 Update dependencies. 2025-02-17 09:26:00 +00:00
Jim McDonald
842d603524 Update workflow. 2025-02-16 15:43:20 +00:00
Jim McDonald
c3471240a5 Update workflows. 2025-02-16 15:42:52 +00:00
Chris Berry
7b4ea7e27e Handle electra attestations for epoch summary 2025-02-14 11:54:05 +00:00
21 changed files with 825 additions and 114 deletions

View File

@@ -8,6 +8,8 @@ on:
permissions:
contents: 'read'
pull-requests: 'read'
checks: 'write'
jobs:
golangci:
@@ -21,4 +23,7 @@ jobs:
- uses: 'actions/checkout@v4'
- uses: 'golangci/golangci-lint-action@v6'
with:
version: 'latest'
args: '--timeout=60m'
only-new-issues: true
skip-cache: true

View File

@@ -74,35 +74,9 @@ run:
# output configuration options
output:
# Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
#
# Multiple can be specified by separating them by comma, output can be provided
# for each of them by separating format name and path by colon symbol.
# Output path can be either `stdout`, `stderr` or path to the file to write to.
# Example: "checkstyle:report.json,colored-line-number"
#
# Default: colored-line-number
# format: json
# Print lines of code with issue.
# Default: true
# print-issued-lines: false
# Print linter name in the end of issue text.
# Default: true
# print-linter-name: false
# Make issues output unique by line.
# Default: true
# uniq-by-line: false
# Add a prefix to the output file references.
# Default is no prefix.
# path-prefix: ""
# Sort results by: filepath, line and column.
# sort-results: true
formats:
- format: colored-line-number
path: stderr
# All available settings of specific linters.
linters-settings:
@@ -139,7 +113,6 @@ linters:
- errorlint
- exhaustive
- exhaustruct
- exportloopref
- forbidigo
- forcetypeassert
- funlen
@@ -162,6 +135,7 @@ linters:
- promlinter
- rowserrcheck
- sqlclosecheck
- tenv
- unparam
- varnamelen
- wastedassign

View File

@@ -1,3 +1,10 @@
1.37.2:
- add "block trail"
1.37.1:
- handle missing blobs for block info
- fix `--epoch` flag for epoch summary
1.37.0:
- support Electra
- add `--compounding` flag when creating validator deposit data

View File

@@ -79,9 +79,9 @@ func outputBlockGeneral(ctx context.Context,
res.WriteString(fmt.Sprintf("Epoch: %d\n", phase0.Epoch(uint64(slot)/slotsPerEpoch)))
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(slot)*int64(slotDuration.Seconds()), 0)))
res.WriteString(fmt.Sprintf("Block root: %#x\n", blockRoot))
res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot))
if verbose {
res.WriteString(fmt.Sprintf("Body root: %#x\n", bodyRoot))
res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot))
res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot))
}
res.WriteString(blockGraffiti(ctx, graffiti))
@@ -607,7 +607,7 @@ func outputDenebBlockText(ctx context.Context,
}
res.WriteString(tmp)
tmp, err = outputBlobInfo(ctx, data.verbose, blobs)
tmp, err = outputBlobInfo(ctx, data.verbose, signedBlock.Message.Body.BlobKZGCommitments, blobs)
if err != nil {
return "", err
}
@@ -717,7 +717,7 @@ func outputElectraBlockText(ctx context.Context,
}
res.WriteString(tmp)
tmp, err = outputBlobInfo(ctx, data.verbose, blobs)
tmp, err = outputBlobInfo(ctx, data.verbose, signedBlock.Message.Body.BlobKZGCommitments, blobs)
if err != nil {
return "", err
}
@@ -1160,6 +1160,7 @@ func outputElectraBlockExecutionRequests(_ context.Context,
func outputBlobInfo(_ context.Context,
verbose bool,
commitments []deneb.KZGCommitment,
blobs []*deneb.BlobSidecar,
) (
string,
@@ -1167,13 +1168,16 @@ func outputBlobInfo(_ context.Context,
) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Blobs: %d\n", len(blobs)))
if verbose {
for i, blob := range blobs {
res.WriteString(fmt.Sprintf("%3d:\n", i))
res.WriteString(fmt.Sprintf(" KZG proof: %s\n", blob.KZGProof.String()))
res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", blob.KZGCommitment.String()))
if len(blobs) == 0 && len(commitments) > 0 {
res.WriteString(fmt.Sprintf("Blobs: %d (but no blobs obtained from the beacon node)\n", len(commitments)))
} else {
res.WriteString(fmt.Sprintf("Blobs: %d\n", len(blobs)))
if verbose {
for i, blob := range blobs {
res.WriteString(fmt.Sprintf("%3d:\n", i))
res.WriteString(fmt.Sprintf(" KZG proof: %s\n", blob.KZGProof.String()))
res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", blob.KZGCommitment.String()))
}
}
}

View File

@@ -168,9 +168,13 @@ func processDenebBlock(ctx context.Context,
Block: data.blockID,
})
if err != nil {
return errors.Wrap(err, "failed to obtain blob sidecars")
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode != http.StatusNotFound {
return errors.Wrap(err, "failed to obtain blob sidecars")
}
} else {
blobSidecars = blobSidecarsResponse.Data
}
blobSidecars = blobSidecarsResponse.Data
}
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
return errors.Wrap(err, "failed to output block")
@@ -193,9 +197,13 @@ func processElectraBlock(ctx context.Context,
Block: data.blockID,
})
if err != nil {
return errors.Wrap(err, "failed to obtain blob sidecars")
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode != http.StatusNotFound {
return errors.Wrap(err, "failed to obtain blob sidecars")
}
} else {
blobSidecars = blobSidecarsResponse.Data
}
blobSidecars = blobSidecarsResponse.Data
}
if err := outputElectraBlock(ctx, data.jsonOutput, data.sszOutput, block.Electra, blobSidecars); err != nil {
return errors.Wrap(err, "failed to output block")

View File

@@ -0,0 +1,89 @@
// Copyright © 2025 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 blocktrail
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Operation.
blockID string
jsonOutput bool
target string
maxBlocks int
// Data access.
consensusClient eth2client.Service
chainTime chaintime.Service
blocksProvider eth2client.SignedBeaconBlockProvider
blockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Processing.
justifiedCheckpoint *phase0.Checkpoint
finalizedCheckpoint *phase0.Checkpoint
// Results.
steps []*step
found bool
}
type step struct {
Slot phase0.Slot `json:"slot"`
Root phase0.Root `json:"root"`
ParentRoot phase0.Root `json:"parent_root"`
State string `json:"state,omitempty"`
// Not a slot, but we're using it to steal the JSON processing.
ExecutionBlock phase0.Slot `json:"execution_block"`
ExecutionHash phase0.Hash32 `json:"execution_hash"`
}
func newCommand(_ context.Context) (*command, error) {
c := &command{
timeout: viper.GetDuration("timeout"),
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
jsonOutput: viper.GetBool("json"),
connection: viper.GetString("connection"),
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
blockID: viper.GetString("blockid"),
target: viper.GetString("target"),
maxBlocks: viper.GetInt("max-blocks"),
steps: make([]*step, 0),
}
// Timeout.
if c.timeout == 0 {
return nil, errors.New("timeout is required")
}
return c, nil
}

74
cmd/block/trail/output.go Normal file
View File

@@ -0,0 +1,74 @@
// Copyright © 2025 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 blocktrail
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 simpleOut struct {
Start *step `json:"start"`
End *step `json:"end"`
Steps int `json:"distance"`
}
func (c *command) outputJSON(_ context.Context) (string, error) {
var err error
var data []byte
if c.verbose {
data, err = json.Marshal(c.steps)
} else {
basic := &simpleOut{
Start: c.steps[0],
End: c.steps[len(c.steps)-1],
Steps: len(c.steps) - 1,
}
data, err = json.Marshal(basic)
}
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputTxt(_ context.Context) (string, error) {
if !c.found {
return "Target not found", nil
}
builder := strings.Builder{}
builder.WriteString("Target '")
builder.WriteString(c.target)
builder.WriteString("' found at a distance of ")
builder.WriteString(fmt.Sprintf("%d", len(c.steps)-1))
builder.WriteString(" block(s)")
return builder.String(), nil
}

182
cmd/block/trail/process.go Normal file
View File

@@ -0,0 +1,182 @@
// Copyright © 2025 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 blocktrail
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
untilRoot := phase0.Root{}
var untilBlock phase0.Slot
switch {
case strings.ToLower(c.target) == "justified", strings.ToLower(c.target) == "finalized":
// Nothing to do.
case strings.HasPrefix(c.target, "0x"):
// Assume a root.
if err := json.Unmarshal([]byte(fmt.Sprintf("%q", c.target)), &untilRoot); err != nil {
return err
}
default:
// Assume a block number.
tmp, err := strconv.ParseUint(c.target, 10, 64)
if err != nil {
return err
}
untilBlock = phase0.Slot(tmp)
}
blockID := c.blockID
for range c.maxBlocks {
step := &step{}
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: blockID,
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
return errors.New("empty beacon block")
}
return errors.Wrap(err, "failed to obtain beacon block")
}
block := blockResponse.Data
step.Slot, err = block.Slot()
if err != nil {
return err
}
step.Root, err = block.Root()
if err != nil {
return err
}
step.ParentRoot, err = block.ParentRoot()
if err != nil {
return err
}
executionBlock, err := block.ExecutionBlockNumber()
if err != nil {
return err
}
step.ExecutionBlock = phase0.Slot(executionBlock)
step.ExecutionHash, err = block.ExecutionBlockHash()
if err != nil {
return err
}
if c.debug {
data, err := json.Marshal(step)
if err == nil {
fmt.Fprintf(os.Stderr, "Step is %s\n", string(data))
}
}
c.steps = append(c.steps, step)
blockID = step.ParentRoot.String()
if c.target == "justified" && bytes.Equal(step.Root[:], c.justifiedCheckpoint.Root[:]) {
c.found = true
break
}
if c.target == "finalized" && bytes.Equal(step.Root[:], c.finalizedCheckpoint.Root[:]) {
c.found = true
break
}
if untilBlock > 0 && step.Slot == untilBlock {
c.found = true
break
}
if (!untilRoot.IsZero()) && bytes.Equal(step.Root[:], untilRoot[:]) {
c.found = true
break
}
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.consensusClient, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.consensusClient.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(c.consensusClient.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.blocksProvider, isProvider = c.consensusClient.(eth2client.SignedBeaconBlockProvider)
if !isProvider {
return errors.New("connection does not provide signed beacon block information")
}
c.blockHeadersProvider, isProvider = c.consensusClient.(eth2client.BeaconBlockHeadersProvider)
if !isProvider {
return errors.New("connection does not provide beacon block header information")
}
finalityProvider, isProvider := c.consensusClient.(eth2client.FinalityProvider)
if !isProvider {
return errors.New("connection does not provide finality information")
}
finalityResponse, err := finalityProvider.Finality(ctx, &api.FinalityOpts{
State: "head",
})
if err != nil {
return errors.Wrap(err, "failed to obtain finality")
}
finality := finalityResponse.Data
c.justifiedCheckpoint = finality.Justified
if c.debug {
fmt.Fprintf(os.Stderr, "Justified checkpoint is %d / %#x\n", c.justifiedCheckpoint.Epoch, c.justifiedCheckpoint.Root)
}
c.finalizedCheckpoint = finality.Finalized
if c.debug {
fmt.Fprintf(os.Stderr, "Finalized checkpoint is %d / %#x\n", c.finalizedCheckpoint.Epoch, c.finalizedCheckpoint.Root)
}
return nil
}

View File

@@ -0,0 +1,63 @@
// Copyright © 2025 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 blocktrail
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: "NoBlock",
vars: map[string]interface{}{
"timeout": "60s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"blockid": "invalid",
},
err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"BAD_REQUEST: Unsupported endpoint version: v2\",\"stacktraces\":[]}",
},
}
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)
}
})
}
}

59
cmd/block/trail/run.go Normal file
View File

@@ -0,0 +1,59 @@
// Copyright © 2025 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 blocktrail
import (
"context"
"errors"
"os"
"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.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
if c.found {
return "", nil
}
os.Exit(1)
}
results, err := c.output(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil
}

65
cmd/blocktrail.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright © 2025 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"
blocktrail "github.com/wealdtech/ethdo/cmd/block/trail"
)
var blockTrailCmd = &cobra.Command{
Use: "trail",
Short: "Trail back in the chain from a given block.",
Long: `Trail back in the chain for a given block. For example:
ethdo block trail --blockid=12345 --target=finalized
In quiet mode this will return 0 if the block trail ends up at the finalized state, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := blocktrail.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
blockCmd.AddCommand(blockTrailCmd)
blockFlags(blockTrailCmd)
blockTrailCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockTrailCmd.Flags().String("target", "justified", "the target block (block number, hash, justified or finalized)")
blockTrailCmd.Flags().Int("max-blocks", 16384, "the maximum number of blocks to look at before halting")
}
func blockTrailBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("target", cmd.Flags().Lookup("target")); err != nil {
panic(err)
}
if err := viper.BindPFlag("max-blocks", cmd.Flags().Lookup("max-blocks")); err != nil {
panic(err)
}
}

View File

@@ -65,6 +65,12 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
errCheck(err, "Failed to obtain finality information")
finality := finalityResponse.Data
blockProvider, isProvider := eth2Client.(eth2client.SignedBeaconBlockProvider)
assert(isProvider, "beacon node does not provide signed beacon blocks; cannot report on chain status")
blockResponse, err := blockProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{Block: "head"})
errCheck(err, "Failed to obtain block information")
block := blockResponse.Data
slot := chainTime.CurrentSlot()
nextSlot := slot + 1
@@ -78,12 +84,28 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
nextEpochStartSlot := chainTime.FirstSlotOfEpoch(nextEpoch)
nextEpochTimestamp := chainTime.StartOfEpoch(nextEpoch)
headSlot, err := block.Slot()
errCheck(err, "Failed to obtain block slot")
res := strings.Builder{}
res.WriteString("Current slot: ")
res.WriteString(fmt.Sprintf("%d", slot))
res.WriteString("\n")
res.WriteString("Head slot: ")
res.WriteString(fmt.Sprintf("%d", headSlot))
if headSlot != slot {
if slot-headSlot == 1 {
res.WriteString("(1 slot behind)")
} else {
res.WriteString(" (")
res.WriteString(fmt.Sprintf("%d", slot-headSlot))
res.WriteString(" slots behind)")
}
}
res.WriteString("\n")
res.WriteString("Current epoch: ")
res.WriteString(fmt.Sprintf("%d", epoch))
res.WriteString("\n")

View File

@@ -45,7 +45,7 @@ var depositVerifyCmd = &cobra.Command{
Short: "Verify deposit data matches the provided data",
Long: `Verify deposit data matches the provided input data. For example:
ethdo deposit verify --data=depositdata.json --withdrawalaccount=primary/current --value="32 Ether"
ethdo deposit verify --data=depositdata.json --withdrawalaccount=primary/current --depositvalue="32 Ether"
The deposit data is compared to the supplied withdrawal account/public key, validator public key, and value to ensure they match.

View File

@@ -34,7 +34,7 @@ func epochFlags(cmd *cobra.Command) {
}
func epochBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
}

View File

@@ -281,66 +281,17 @@ func (c *command) processSlots(ctx context.Context,
}
slotCommittees = allCommittees[attestationData.Slot]
}
committee := slotCommittees[attestationData.Index]
inclusionDistance := slot - attestationData.Slot
head, err := util.AttestationHead(ctx, headersCache, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
target, err := util.AttestationTarget(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "failed to obtain aggregation bits")
}
for i := range aggregationBits.Len() {
if aggregationBits.BitAt(i) {
validatorIndex := committee[int(i)]
if len(c.validators) > 0 {
if _, exists := c.validators[validatorIndex]; !exists {
// Not one of our validators.
continue
}
}
// Only set the information from the first attestation we find for this validator.
if participations[validatorIndex].InclusionSlot == 0 {
participations[validatorIndex].HeadVote = &attestationData.BeaconBlockRoot
participations[validatorIndex].Head = &head
participations[validatorIndex].TargetVote = &attestationData.Target.Root
participations[validatorIndex].Target = &target
participations[validatorIndex].InclusionSlot = slot
}
votes[validatorIndex] = struct{}{}
if _, exists := headCorrects[validatorIndex]; !exists && headCorrect {
headCorrects[validatorIndex] = struct{}{}
}
if _, exists := headTimelys[validatorIndex]; !exists && headCorrect && inclusionDistance == 1 {
headTimelys[validatorIndex] = struct{}{}
}
if _, exists := sourceTimelys[validatorIndex]; !exists && inclusionDistance <= 5 {
sourceTimelys[validatorIndex] = struct{}{}
}
if _, exists := targetCorrects[validatorIndex]; !exists && targetCorrect {
targetCorrects[validatorIndex] = struct{}{}
}
if _, exists := targetTimelys[validatorIndex]; !exists && targetCorrect && inclusionDistance <= 32 {
targetTimelys[validatorIndex] = struct{}{}
}
if attestation.Version >= spec.DataVersionElectra {
participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys, err = c.extractElectraAttestationData(
ctx, attestation, attestationData, slotCommittees, slot, headersCache, participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
} else {
participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys, err = c.extractPhase0AttestationData(
ctx, attestation, attestationData, slotCommittees, slot, headersCache, participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
}
}
@@ -356,6 +307,201 @@ func (c *command) processSlots(ctx context.Context,
nil
}
func (c *command) extractPhase0AttestationData(ctx context.Context,
attestation *spec.VersionedAttestation,
attestationData *phase0.AttestationData,
slotCommittees map[phase0.CommitteeIndex][]phase0.ValidatorIndex,
slot phase0.Slot,
headersCache *util.BeaconBlockHeaderCache,
participations map[phase0.ValidatorIndex]*attestingValidator,
votes map[phase0.ValidatorIndex]struct{},
headCorrects map[phase0.ValidatorIndex]struct{},
headTimelys map[phase0.ValidatorIndex]struct{},
sourceTimelys map[phase0.ValidatorIndex]struct{},
targetCorrects map[phase0.ValidatorIndex]struct{},
targetTimelys map[phase0.ValidatorIndex]struct{},
) (
map[phase0.ValidatorIndex]*attestingValidator,
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
error,
) {
committee := slotCommittees[attestationData.Index]
inclusionDistance := slot - attestationData.Slot
head, err := util.AttestationHead(ctx, headersCache, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
target, err := util.AttestationTarget(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "failed to obtain aggregation bits")
}
for i := range aggregationBits.Len() {
if aggregationBits.BitAt(i) {
validatorIndex := committee[int(i)]
if len(c.validators) > 0 {
if _, exists := c.validators[validatorIndex]; !exists {
// Not one of our validators.
continue
}
}
// Only set the information from the first attestation we find for this validator.
if participations[validatorIndex].InclusionSlot == 0 {
participations[validatorIndex].HeadVote = &attestationData.BeaconBlockRoot
participations[validatorIndex].Head = &head
participations[validatorIndex].TargetVote = &attestationData.Target.Root
participations[validatorIndex].Target = &target
participations[validatorIndex].InclusionSlot = slot
}
votes[validatorIndex] = struct{}{}
if _, exists := headCorrects[validatorIndex]; !exists && headCorrect {
headCorrects[validatorIndex] = struct{}{}
}
if _, exists := headTimelys[validatorIndex]; !exists && headCorrect && inclusionDistance == 1 {
headTimelys[validatorIndex] = struct{}{}
}
if _, exists := sourceTimelys[validatorIndex]; !exists && inclusionDistance <= 5 {
sourceTimelys[validatorIndex] = struct{}{}
}
if _, exists := targetCorrects[validatorIndex]; !exists && targetCorrect {
targetCorrects[validatorIndex] = struct{}{}
}
if _, exists := targetTimelys[validatorIndex]; !exists && targetCorrect && inclusionDistance <= 32 {
targetTimelys[validatorIndex] = struct{}{}
}
}
}
return participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys, err
}
func (c *command) extractElectraAttestationData(ctx context.Context,
attestation *spec.VersionedAttestation,
attestationData *phase0.AttestationData,
slotCommittees map[phase0.CommitteeIndex][]phase0.ValidatorIndex,
slot phase0.Slot,
headersCache *util.BeaconBlockHeaderCache,
participations map[phase0.ValidatorIndex]*attestingValidator,
votes map[phase0.ValidatorIndex]struct{},
headCorrects map[phase0.ValidatorIndex]struct{},
headTimelys map[phase0.ValidatorIndex]struct{},
sourceTimelys map[phase0.ValidatorIndex]struct{},
targetCorrects map[phase0.ValidatorIndex]struct{},
targetTimelys map[phase0.ValidatorIndex]struct{},
) (
map[phase0.ValidatorIndex]*attestingValidator,
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]struct{},
error,
) {
committeeBits, err := attestation.CommitteeBits()
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "failed to obtain committee bits")
}
for _, committeeIndex := range committeeBits.BitIndices() {
committee := slotCommittees[phase0.CommitteeIndex(committeeIndex)]
inclusionDistance := slot - attestationData.Slot
head, err := util.AttestationHead(ctx, headersCache, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
target, err := util.AttestationTarget(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err
}
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "failed to obtain aggregation bits")
}
// Calculate the offset for the committee so we can extract the validator from the aggregate_bits.
committeeOffset := calcCommitteeOffset(phase0.CommitteeIndex(committeeIndex), slotCommittees)
// Range over the committee rather than the aggregate_bits as it's the smaller set.
for i := range committee {
aggregateIndex := committeeOffset + uint64(i)
if aggregationBits.BitAt(aggregateIndex) {
validatorIndex := committee[i]
if len(c.validators) > 0 {
if _, exists := c.validators[validatorIndex]; !exists {
// Not one of our validators.
continue
}
}
// Only set the information from the first attestation we find for this validator.
if participations[validatorIndex].InclusionSlot == 0 {
participations[validatorIndex].HeadVote = &attestationData.BeaconBlockRoot
participations[validatorIndex].Head = &head
participations[validatorIndex].TargetVote = &attestationData.Target.Root
participations[validatorIndex].Target = &target
participations[validatorIndex].InclusionSlot = slot
}
votes[validatorIndex] = struct{}{}
if _, exists := headCorrects[validatorIndex]; !exists && headCorrect {
headCorrects[validatorIndex] = struct{}{}
}
if _, exists := headTimelys[validatorIndex]; !exists && headCorrect && inclusionDistance == 1 {
headTimelys[validatorIndex] = struct{}{}
}
if _, exists := sourceTimelys[validatorIndex]; !exists && inclusionDistance <= 5 {
sourceTimelys[validatorIndex] = struct{}{}
}
if _, exists := targetCorrects[validatorIndex]; !exists && targetCorrect {
targetCorrects[validatorIndex] = struct{}{}
}
if _, exists := targetTimelys[validatorIndex]; !exists && targetCorrect && inclusionDistance <= 32 {
targetTimelys[validatorIndex] = struct{}{}
}
}
}
}
return participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys, err
}
func calcCommitteeOffset(committeeIndex phase0.CommitteeIndex, slotCommittees map[phase0.CommitteeIndex][]phase0.ValidatorIndex) uint64 {
var total uint64
for i := range committeeIndex {
total += uint64(len(slotCommittees[i]))
}
return total
}
func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
if c.summary.Epoch < c.chainTime.AltairInitialEpoch() {
// The epoch is pre-Altair. No info but no error.

View File

@@ -52,6 +52,7 @@ var bindings = map[string]func(cmd *cobra.Command){
"attester/inclusion": attesterInclusionBindings,
"block/analyze": blockAnalyzeBindings,
"block/info": blockInfoBindings,
"block/trail": blockTrailBindings,
"chain/eth1votes": chainEth1VotesBindings,
"chain/info": chainInfoBindings,
"chain/queues": chainQueuesBindings,

View File

@@ -26,7 +26,7 @@ var validatorDepositDataCmd = &cobra.Command{
Short: "Generate deposit data for one or more validators",
Long: `Generate data for deposits to the Ethereum 1 validator contract. For example:
ethdo validator depositdata --validatoraccount=primary/validator --withdrawalaccount=primary/current --value="32 Ether"
ethdo validator depositdata --validatoraccount=primary/validator --withdrawalaccount=primary/current --depositvalue="32 Ether"
If validatoraccount is provided with an account path it will generate deposit data for all matching accounts.

View File

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

View File

@@ -371,6 +371,18 @@ Deposits: 0
Voluntary exits: 0
```
#### `trail`
`ethdo block trail` tracks back from the provided block to see if it is in a chain descending from the a target block. Options include:
- `blockid`: the ID (slot, root, 'head') of the block to trail from; defaults to head
- `target`: the target block (slot, block hash, 'justified', 'finalized') to check; defaults to 'justified'
- `max-blocks`: the maximum number of blocks to look at to find the target
```sh
$ ethdo block trail
Target 'justified' found at a distance of 54 block(s)
```
### `chain` commands
Chain commands focus on providing information about Ethereum consensus chains.

4
go.mod
View File

@@ -10,7 +10,7 @@ require (
github.com/gofrs/uuid v4.4.0+incompatible
github.com/google/uuid v1.6.0
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/herumi/bls-eth-go-binary v1.36.1
github.com/herumi/bls-eth-go-binary v1.36.4
github.com/mitchellh/go-homedir v1.1.0
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pkg/errors v0.9.1
@@ -31,7 +31,7 @@ require (
github.com/wealdtech/go-eth2-wallet-dirk v1.5.1
github.com/wealdtech/go-eth2-wallet-distributed v1.2.1
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.0
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.1
github.com/wealdtech/go-eth2-wallet-keystore v1.0.0
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.0
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.18.1

8
go.sum
View File

@@ -48,8 +48,8 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsD
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/herumi/bls-eth-go-binary v1.36.1 h1:SfLjxbO1fWkKtKS7J3Ezd1/5QXrcaTZgWynxdSe10hQ=
github.com/herumi/bls-eth-go-binary v1.36.1/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U=
github.com/herumi/bls-eth-go-binary v1.36.4 h1:yff41RSbfyZwfE1NF/qddP5nXhgdU0c3RGOpYOoM7YM=
github.com/herumi/bls-eth-go-binary v1.36.4/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c=
@@ -178,8 +178,8 @@ github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1 h1:9j7bpwjT9wmwB
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1/go.mod h1:+tI1VD76E1WINI+Nstg7RVGpUolL5ql10nu2YztMO/4=
github.com/wealdtech/go-eth2-wallet-encryptor-unencrypted v1.0.2 h1:IMIyl70hbJlxOkgTcCK//3vKe5ylhGIk6oUlIlK9xp0=
github.com/wealdtech/go-eth2-wallet-encryptor-unencrypted v1.0.2/go.mod h1:T8nyAscWIWNcNa6EG/19PwH/OCt2Ly7Orn5okmiuSP4=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.0 h1:5g4emFacTf+sX6zx6SbZIZGR7Jx5Xr/Xdb7sXnEXlWk=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.0/go.mod h1:aWgnEi07w1L9wMBRB69sYvoEONppAUly6FDQRWQGqH8=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.1 h1:CrcPeJhMcNxSW+GAJwtpXz3mtGJjx4p9ykLlKvwZZZ4=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.1/go.mod h1:aWgnEi07w1L9wMBRB69sYvoEONppAUly6FDQRWQGqH8=
github.com/wealdtech/go-eth2-wallet-keystore v1.0.0 h1:DYR6TAyi7RxXoAanLSPdiufGxCX617BQwWOdCxHqHX4=
github.com/wealdtech/go-eth2-wallet-keystore v1.0.0/go.mod h1:6DGINunnasS9y9F7KH3ya2h74fHWgSCfP3dAJWe4A6U=
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.0 h1:vphAFklkYMRJVo9f5rVWly7PECHrLS4yarjemBa7fRM=