mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-08 21:48:05 -05:00
Add "block trail".
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
1.37.2:
|
||||||
|
- add "block trail"
|
||||||
|
|
||||||
1.37.1:
|
1.37.1:
|
||||||
- handle missing blobs for block info
|
- handle missing blobs for block info
|
||||||
- fix `--epoch` flag for epoch summary
|
- fix `--epoch` flag for epoch summary
|
||||||
|
|||||||
@@ -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("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("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("Block root: %#x\n", blockRoot))
|
||||||
|
res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot))
|
||||||
if verbose {
|
if verbose {
|
||||||
res.WriteString(fmt.Sprintf("Body root: %#x\n", bodyRoot))
|
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(fmt.Sprintf("State root: %#x\n", stateRoot))
|
||||||
}
|
}
|
||||||
res.WriteString(blockGraffiti(ctx, graffiti))
|
res.WriteString(blockGraffiti(ctx, graffiti))
|
||||||
|
|||||||
89
cmd/block/trail/command.go
Normal file
89
cmd/block/trail/command.go
Normal 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
74
cmd/block/trail/output.go
Normal 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
182
cmd/block/trail/process.go
Normal 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
|
||||||
|
}
|
||||||
63
cmd/block/trail/process_internal_test.go
Normal file
63
cmd/block/trail/process_internal_test.go
Normal 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
59
cmd/block/trail/run.go
Normal 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
65
cmd/blocktrail.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
errCheck(err, "Failed to obtain finality information")
|
||||||
finality := finalityResponse.Data
|
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()
|
slot := chainTime.CurrentSlot()
|
||||||
|
|
||||||
nextSlot := slot + 1
|
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)
|
nextEpochStartSlot := chainTime.FirstSlotOfEpoch(nextEpoch)
|
||||||
nextEpochTimestamp := chainTime.StartOfEpoch(nextEpoch)
|
nextEpochTimestamp := chainTime.StartOfEpoch(nextEpoch)
|
||||||
|
|
||||||
|
headSlot, err := block.Slot()
|
||||||
|
errCheck(err, "Failed to obtain block slot")
|
||||||
|
|
||||||
res := strings.Builder{}
|
res := strings.Builder{}
|
||||||
|
|
||||||
res.WriteString("Current slot: ")
|
res.WriteString("Current slot: ")
|
||||||
res.WriteString(fmt.Sprintf("%d", slot))
|
res.WriteString(fmt.Sprintf("%d", slot))
|
||||||
res.WriteString("\n")
|
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("Current epoch: ")
|
||||||
res.WriteString(fmt.Sprintf("%d", epoch))
|
res.WriteString(fmt.Sprintf("%d", epoch))
|
||||||
res.WriteString("\n")
|
res.WriteString("\n")
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ var bindings = map[string]func(cmd *cobra.Command){
|
|||||||
"attester/inclusion": attesterInclusionBindings,
|
"attester/inclusion": attesterInclusionBindings,
|
||||||
"block/analyze": blockAnalyzeBindings,
|
"block/analyze": blockAnalyzeBindings,
|
||||||
"block/info": blockInfoBindings,
|
"block/info": blockInfoBindings,
|
||||||
|
"block/trail": blockTrailBindings,
|
||||||
"chain/eth1votes": chainEth1VotesBindings,
|
"chain/eth1votes": chainEth1VotesBindings,
|
||||||
"chain/info": chainInfoBindings,
|
"chain/info": chainInfoBindings,
|
||||||
"chain/queues": chainQueuesBindings,
|
"chain/queues": chainQueuesBindings,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
|
|
||||||
// ReleaseVersion is the release version of the codebase.
|
// ReleaseVersion is the release version of the codebase.
|
||||||
// Usually overridden by tag names when building binaries.
|
// Usually overridden by tag names when building binaries.
|
||||||
var ReleaseVersion = "local build (latest release 1.37.1)"
|
var ReleaseVersion = "local build (latest release 1.37.2)"
|
||||||
|
|
||||||
// versionCmd represents the version command.
|
// versionCmd represents the version command.
|
||||||
var versionCmd = &cobra.Command{
|
var versionCmd = &cobra.Command{
|
||||||
|
|||||||
@@ -371,6 +371,18 @@ Deposits: 0
|
|||||||
Voluntary exits: 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
|
||||||
|
|
||||||
Chain commands focus on providing information about Ethereum consensus chains.
|
Chain commands focus on providing information about Ethereum consensus chains.
|
||||||
|
|||||||
Reference in New Issue
Block a user