mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d3bd966e0 | ||
|
|
0488b13ba1 | ||
|
|
f8a611d63d | ||
|
|
772f07f8ec | ||
|
|
f6e23d803b | ||
|
|
86e872294d | ||
|
|
d7d9c66052 | ||
|
|
3e173f141e | ||
|
|
5d95e93b76 | ||
|
|
34b752edcc | ||
|
|
f17fe2f5cb | ||
|
|
8322353af5 | ||
|
|
7d0e607f96 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -15,6 +15,12 @@ coverage.html
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
# Intellij
|
||||
.idea/
|
||||
|
||||
# Makefile
|
||||
Makefile
|
||||
|
||||
# Vim
|
||||
*.sw?
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
1.37.0:
|
||||
- support Electra
|
||||
- add `--compounding` flag when creating validator deposit data
|
||||
|
||||
1.36.6:
|
||||
- allow specification of blockid for validator info
|
||||
- validator depositdata orders deposits from an HD wallet by path
|
||||
|
||||
1.36.5:
|
||||
- avoid corner case mnemonic derivation with 25th word
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22-bookworm as builder
|
||||
FROM golang:1.23-bookworm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -27,7 +28,7 @@ type dataOut struct {
|
||||
debug bool
|
||||
quiet bool
|
||||
verbose bool
|
||||
attestation *phase0.Attestation
|
||||
attestation *spec.VersionedAttestation
|
||||
slot phase0.Slot
|
||||
attestationIndex uint64
|
||||
inclusionDelay phase0.Slot
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
@@ -93,9 +94,18 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return nil, errors.Wrap(err, "failed to obtain block attestations")
|
||||
}
|
||||
for i, attestation := range attestations {
|
||||
if attestation.Data.Slot == duty.Slot &&
|
||||
attestation.Data.Index == duty.CommitteeIndex &&
|
||||
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
aggregationBits, err := attestation.AggregationBits()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attestation aggregation bits")
|
||||
}
|
||||
|
||||
if attestationData.Slot == duty.Slot &&
|
||||
attestationData.Index == duty.CommitteeIndex &&
|
||||
aggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
||||
headCorrect := false
|
||||
targetCorrect := false
|
||||
if data.verbose {
|
||||
@@ -128,8 +138,13 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
|
||||
slot := attestation.Data.Slot
|
||||
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *spec.VersionedAttestation) (bool, error) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
slot := attestationData.Slot
|
||||
for {
|
||||
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
@@ -149,13 +164,19 @@ func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Atte
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
return bytes.Equal(response.Data.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
|
||||
|
||||
return bytes.Equal(response.Data.Root[:], attestationData.BeaconBlockRoot[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
|
||||
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *spec.VersionedAttestation) (bool, error) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
// Start with first slot of the target epoch.
|
||||
slot := data.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
|
||||
slot := data.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
|
||||
for {
|
||||
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
@@ -175,7 +196,8 @@ func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.At
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
return bytes.Equal(response.Data.Root[:], attestation.Data.Target.Root[:]), nil
|
||||
|
||||
return bytes.Equal(response.Data.Root[:], attestationData.Target.Root[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ type command struct {
|
||||
weightDenominator uint64
|
||||
|
||||
// Processing.
|
||||
priorAttestations map[string]*attestationData
|
||||
priorAttestations map[string]*attestationDataInfo
|
||||
// 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.
|
||||
@@ -77,20 +77,20 @@ type blockAnalysis struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
Head phase0.Root `json:"head"`
|
||||
Target phase0.Root `json:"target"`
|
||||
Distance int `json:"distance"`
|
||||
Duplicate *attestationDataInfo `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 {
|
||||
@@ -100,7 +100,7 @@ type syncCommitteeAnalysis struct {
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type attestationData struct {
|
||||
type attestationDataInfo struct {
|
||||
Block phase0.Slot `json:"block"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func newCommand(_ context.Context) (*command, error) {
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
priorAttestations: make(map[string]*attestationData),
|
||||
priorAttestations: make(map[string]*attestationDataInfo),
|
||||
headRoots: make(map[phase0.Slot]phase0.Root),
|
||||
targetRoots: make(map[phase0.Slot]phase0.Root),
|
||||
votes: make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist),
|
||||
|
||||
@@ -34,20 +34,20 @@ func (c *command) output(ctx context.Context) (string, error) {
|
||||
}
|
||||
|
||||
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"`
|
||||
Head string `json:"head"`
|
||||
Target string `json:"target"`
|
||||
Distance int `json:"distance"`
|
||||
Duplicate *attestationDataInfo `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) {
|
||||
|
||||
@@ -63,8 +63,12 @@ func (c *command) process(ctx context.Context) error {
|
||||
// Calculate how many parents we need to fetch.
|
||||
minSlot := slot
|
||||
for _, attestation := range attestations {
|
||||
if attestation.Data.Slot < minSlot {
|
||||
minSlot = attestation.Data.Slot
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
if attestationData.Slot < minSlot {
|
||||
minSlot = attestationData.Slot
|
||||
}
|
||||
}
|
||||
if c.debug {
|
||||
@@ -103,10 +107,16 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
|
||||
if c.debug {
|
||||
fmt.Printf("Processing attestation %d\n", i)
|
||||
}
|
||||
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
analysis := &attestationAnalysis{
|
||||
Head: attestation.Data.BeaconBlockRoot,
|
||||
Target: attestation.Data.Target.Root,
|
||||
Distance: int(slot - attestation.Data.Slot),
|
||||
Head: attestationData.BeaconBlockRoot,
|
||||
Target: attestationData.Target.Root,
|
||||
Distance: int(slot - attestationData.Slot),
|
||||
}
|
||||
|
||||
root, err := attestation.HashTreeRoot()
|
||||
@@ -116,45 +126,47 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
|
||||
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)
|
||||
aggregationBits, err := attestation.AggregationBits()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, exists = blockVotes[data.Slot][data.Index]
|
||||
_, exists := blockVotes[attestationData.Slot]
|
||||
if !exists {
|
||||
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
|
||||
blockVotes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
|
||||
}
|
||||
_, exists = blockVotes[attestationData.Slot][attestationData.Index]
|
||||
if !exists {
|
||||
blockVotes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
|
||||
}
|
||||
|
||||
// Count new votes.
|
||||
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
|
||||
for j := range attestation.AggregationBits.Len() {
|
||||
if attestation.AggregationBits.BitAt(j) {
|
||||
analysis.PossibleVotes = int(aggregationBits.Len())
|
||||
for j := range aggregationBits.Len() {
|
||||
if aggregationBits.BitAt(j) {
|
||||
analysis.Votes++
|
||||
if blockVotes[data.Slot][data.Index].BitAt(j) {
|
||||
if blockVotes[attestationData.Slot][attestationData.Index].BitAt(j) {
|
||||
// Already attested to in this block; skip.
|
||||
continue
|
||||
}
|
||||
if c.votes[data.Slot][data.Index].BitAt(j) {
|
||||
if c.votes[attestationData.Slot][attestationData.Index].BitAt(j) {
|
||||
// Already attested to in a previous block; skip.
|
||||
continue
|
||||
}
|
||||
analysis.NewVotes++
|
||||
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
|
||||
blockVotes[attestationData.Slot][attestationData.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 = analysis.HeadCorrect && attestation.Data.Slot == slot-1
|
||||
analysis.HeadTimely = analysis.HeadCorrect && attestationData.Slot == slot-1
|
||||
|
||||
// Calculate source timely.
|
||||
analysis.SourceTimely = attestation.Data.Slot >= slot-5
|
||||
analysis.SourceTimely = attestationData.Slot >= slot-5
|
||||
|
||||
// Calculate target correct.
|
||||
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
|
||||
@@ -164,7 +176,7 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
|
||||
|
||||
// Calculate target timely.
|
||||
if block.Version < spec.DataVersionDeneb {
|
||||
analysis.TargetTimely = attestation.Data.Slot >= slot-32
|
||||
analysis.TargetTimely = attestationData.Slot >= slot-32
|
||||
} else {
|
||||
analysis.TargetTimely = true
|
||||
}
|
||||
@@ -194,7 +206,7 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := block.Deneb.HashTreeRoot()
|
||||
root, err := block.Root()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -255,23 +267,31 @@ func (c *command) processParentBlock(_ context.Context, block *spec.VersionedSig
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
|
||||
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationDataInfo{
|
||||
Block: slot,
|
||||
Index: i,
|
||||
}
|
||||
|
||||
data := attestation.Data
|
||||
_, exists := c.votes[data.Slot]
|
||||
if !exists {
|
||||
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
_, exists = c.votes[data.Slot][data.Index]
|
||||
if !exists {
|
||||
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
|
||||
aggregationBits, err := attestation.AggregationBits()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attestation aggregation bits")
|
||||
}
|
||||
for j := range attestation.AggregationBits.Len() {
|
||||
if attestation.AggregationBits.BitAt(j) {
|
||||
c.votes[data.Slot][data.Index].SetBitAt(j, true)
|
||||
|
||||
_, exists := c.votes[attestationData.Slot]
|
||||
if !exists {
|
||||
c.votes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
|
||||
}
|
||||
_, exists = c.votes[attestationData.Slot][attestationData.Index]
|
||||
if !exists {
|
||||
c.votes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
|
||||
}
|
||||
for j := range aggregationBits.Len() {
|
||||
if aggregationBits.BitAt(j) {
|
||||
c.votes[attestationData.Slot][attestationData.Index].SetBitAt(j, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,8 +405,13 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
|
||||
slot := attestation.Data.Slot
|
||||
func (c *command) calcHeadCorrect(ctx context.Context, attestation *spec.VersionedAttestation) (bool, error) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
slot := attestationData.Slot
|
||||
root, exists := c.headRoots[slot]
|
||||
if !exists {
|
||||
for {
|
||||
@@ -413,20 +438,25 @@ func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attes
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
c.headRoots[attestation.Data.Slot] = response.Data.Root
|
||||
c.headRoots[slot] = response.Data.Root
|
||||
root = response.Data.Root
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
|
||||
return bytes.Equal(root[:], attestationData.BeaconBlockRoot[:]), nil
|
||||
}
|
||||
|
||||
func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
|
||||
root, exists := c.targetRoots[attestation.Data.Slot]
|
||||
func (c *command) calcTargetCorrect(ctx context.Context, attestation *spec.VersionedAttestation) (bool, error) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
root, exists := c.targetRoots[attestationData.Slot]
|
||||
if !exists {
|
||||
// Start with first slot of the target epoch.
|
||||
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
|
||||
slot := c.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
|
||||
for {
|
||||
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
@@ -450,12 +480,12 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
c.targetRoots[attestation.Data.Slot] = response.Data.Root
|
||||
c.targetRoots[attestationData.Slot] = response.Data.Root
|
||||
root = response.Data.Root
|
||||
break
|
||||
}
|
||||
}
|
||||
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
|
||||
return bytes.Equal(root[:], attestationData.Target.Root[:]), nil
|
||||
}
|
||||
|
||||
func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
|
||||
@@ -491,6 +521,13 @@ func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.Versioned
|
||||
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
|
||||
c.analysis.Value += c.analysis.SyncCommitee.Value
|
||||
return nil
|
||||
case spec.DataVersionElectra:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Electra.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Electra.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)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
"github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/deneb"
|
||||
"github.com/attestantio/go-eth2-client/spec/electra"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/go-bitfield"
|
||||
@@ -133,7 +134,61 @@ func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service,
|
||||
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
|
||||
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
|
||||
if _, exists := committees[att.Data.Index]; exists {
|
||||
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
|
||||
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees, []int{int(att.Data.Index)})))
|
||||
}
|
||||
res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot))
|
||||
res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot))
|
||||
res.WriteString(fmt.Sprintf(" Source epoch: %d\n", att.Data.Source.Epoch))
|
||||
res.WriteString(fmt.Sprintf(" Source root: %#x\n", att.Data.Source.Root))
|
||||
res.WriteString(fmt.Sprintf(" Target epoch: %d\n", att.Data.Target.Epoch))
|
||||
res.WriteString(fmt.Sprintf(" Target root: %#x\n", att.Data.Target.Root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputElectraBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*electra.Attestation) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
validatorCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
res.WriteString(fmt.Sprintf("Attestations: %d\n", len(attestations)))
|
||||
if verbose {
|
||||
beaconCommitteesProvider, isProvider := eth2Client.(eth2client.BeaconCommitteesProvider)
|
||||
if isProvider {
|
||||
for i, att := range attestations {
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
|
||||
// Fetch committees for this epoch if not already obtained.
|
||||
committees, exists := validatorCommittees[att.Data.Slot]
|
||||
if !exists {
|
||||
response, err := beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{
|
||||
State: fmt.Sprintf("%d", att.Data.Slot),
|
||||
})
|
||||
if err != nil {
|
||||
// Failed to get it; create an empty committee to stop us continually attempting to re-fetch.
|
||||
validatorCommittees[att.Data.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
} else {
|
||||
for _, beaconCommittee := range response.Data {
|
||||
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
|
||||
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
}
|
||||
validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
|
||||
}
|
||||
}
|
||||
committees = validatorCommittees[att.Data.Slot]
|
||||
}
|
||||
|
||||
committeeIndices := make([]phase0.CommitteeIndex, 0)
|
||||
for _, committeeIndex := range att.CommitteeBits.BitIndices() {
|
||||
committeeIndices = append(committeeIndices, phase0.CommitteeIndex(committeeIndex))
|
||||
}
|
||||
res.WriteString(fmt.Sprintf(" Committee indices: %d\n", committeeIndices))
|
||||
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
|
||||
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
|
||||
if _, exists := committees[att.Data.Index]; exists {
|
||||
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees, att.CommitteeBits.BitIndices())))
|
||||
}
|
||||
res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot))
|
||||
res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot))
|
||||
@@ -198,6 +253,56 @@ func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Ser
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputElectraBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*electra.AttesterSlashing) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings)))
|
||||
if verbose {
|
||||
for i, slashing := range attesterSlashings {
|
||||
// Say what was slashed.
|
||||
att1 := slashing.Attestation1
|
||||
att2 := slashing.Attestation2
|
||||
slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices)
|
||||
if len(slashedIndices) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
res.WriteString(fmt.Sprintln(" Slashed validators:"))
|
||||
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
|
||||
State: "head",
|
||||
Indices: slashedIndices,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain beacon committees")
|
||||
}
|
||||
for k, v := range response.Data {
|
||||
res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k))
|
||||
}
|
||||
|
||||
// Say what caused the slashing.
|
||||
if att1.Data.Target.Epoch == att2.Data.Target.Epoch {
|
||||
res.WriteString(fmt.Sprintf(" Double voted for same target epoch (%d):\n", att1.Data.Target.Epoch))
|
||||
if !bytes.Equal(att1.Data.Target.Root[:], att2.Data.Target.Root[:]) {
|
||||
res.WriteString(fmt.Sprintf(" Attestation 1 target epoch root: %#x\n", att1.Data.Target.Root))
|
||||
res.WriteString(fmt.Sprintf(" Attestation 2target epoch root: %#x\n", att2.Data.Target.Root))
|
||||
}
|
||||
if !bytes.Equal(att1.Data.BeaconBlockRoot[:], att2.Data.BeaconBlockRoot[:]) {
|
||||
res.WriteString(fmt.Sprintf(" Attestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot))
|
||||
res.WriteString(fmt.Sprintf(" Attestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot))
|
||||
}
|
||||
} else if att1.Data.Source.Epoch < att2.Data.Source.Epoch &&
|
||||
att1.Data.Target.Epoch > att2.Data.Target.Epoch {
|
||||
res.WriteString(" Surround voted:\n")
|
||||
res.WriteString(fmt.Sprintf(" Attestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch))
|
||||
res.WriteString(fmt.Sprintf(" Attestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockDeposits(_ context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
@@ -502,7 +607,117 @@ func outputDenebBlockText(ctx context.Context,
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
tmp, err = outputDenebBlobInfo(ctx, data.verbose, signedBlock.Message.Body, blobs)
|
||||
tmp, err = outputBlobInfo(ctx, data.verbose, blobs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputElectraBlockText(ctx context.Context,
|
||||
data *dataOut,
|
||||
signedBlock *electra.SignedBeaconBlock,
|
||||
blobs []*deneb.BlobSidecar,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
if signedBlock == nil {
|
||||
return "", errors.New("no block supplied")
|
||||
}
|
||||
|
||||
body := signedBlock.Message.Body
|
||||
|
||||
res := strings.Builder{}
|
||||
|
||||
// General info.
|
||||
blockRoot, err := signedBlock.Message.HashTreeRoot()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain block root")
|
||||
}
|
||||
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to generate body root")
|
||||
}
|
||||
|
||||
tmp, err := outputBlockGeneral(ctx,
|
||||
data.verbose,
|
||||
signedBlock.Message.Slot,
|
||||
signedBlock.Message.ProposerIndex,
|
||||
blockRoot,
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
signedBlock.Message.StateRoot,
|
||||
signedBlock.Message.Body.Graffiti[:],
|
||||
data.genesisTime,
|
||||
data.slotDuration,
|
||||
data.slotsPerEpoch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Eth1 data.
|
||||
if data.verbose {
|
||||
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
}
|
||||
|
||||
// Sync aggregate.
|
||||
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Attestations.
|
||||
tmp, err = outputElectraBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Attester slashings.
|
||||
tmp, err = outputElectraBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
|
||||
// Add verbose proposer slashings.
|
||||
|
||||
// Voluntary exits.
|
||||
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
tmp, err = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
tmp, err = outputDenebBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
tmp, err = outputElectraBlockExecutionRequests(ctx, data.verbose, signedBlock.Message.Body.ExecutionRequests)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
tmp, err = outputBlobInfo(ctx, data.verbose, blobs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -897,30 +1112,69 @@ func outputDenebBlockExecutionPayload(_ context.Context,
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputDenebBlobInfo(_ context.Context,
|
||||
func outputElectraBlockExecutionRequests(_ context.Context,
|
||||
verbose bool,
|
||||
executionRequests *electra.ExecutionRequests,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
if executionRequests == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
res.WriteString("Deposit requests: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(executionRequests.Deposits)))
|
||||
if verbose {
|
||||
for i, deposit := range executionRequests.Deposits {
|
||||
res.WriteString(fmt.Sprintf("%3d:\n", i))
|
||||
res.WriteString(fmt.Sprintf(" Public key: %#x\n", deposit.Pubkey))
|
||||
res.WriteString(fmt.Sprintf(" Withdrawal credentials: %#x\n", deposit.WithdrawalCredentials))
|
||||
res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(deposit.Amount), true)))
|
||||
}
|
||||
}
|
||||
res.WriteString("Withdrawal requests: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(executionRequests.Withdrawals)))
|
||||
if verbose {
|
||||
for i, withdrawal := range executionRequests.Withdrawals {
|
||||
res.WriteString(fmt.Sprintf("%3d:\n", i))
|
||||
res.WriteString(fmt.Sprintf(" Source address: %#x\n", withdrawal.SourceAddress))
|
||||
res.WriteString(fmt.Sprintf(" Validator public key: %#x\n", withdrawal.ValidatorPubkey))
|
||||
res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(withdrawal.Amount), true)))
|
||||
}
|
||||
}
|
||||
res.WriteString("Consolidation requests: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(executionRequests.Consolidations)))
|
||||
if verbose {
|
||||
for i, consolidation := range executionRequests.Consolidations {
|
||||
res.WriteString(fmt.Sprintf("%3d:\n", i))
|
||||
res.WriteString(fmt.Sprintf(" Source address: %#x\n", consolidation.SourceAddress))
|
||||
res.WriteString(fmt.Sprintf(" Source public key: %#x\n", consolidation.SourcePubkey))
|
||||
res.WriteString(fmt.Sprintf(" Target public key: %#x\n", consolidation.TargetPubkey))
|
||||
}
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlobInfo(_ context.Context,
|
||||
verbose bool,
|
||||
body *deneb.BeaconBlockBody,
|
||||
blobs []*deneb.BlobSidecar,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
if body == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !verbose {
|
||||
return fmt.Sprintf("Blobs: %d\n", len(body.BlobKZGCommitments)), nil
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
|
||||
for i, blob := range blobs {
|
||||
if i == 0 {
|
||||
res.WriteString("Blobs\n")
|
||||
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()))
|
||||
}
|
||||
res.WriteString(fmt.Sprintf(" Index: %d\n", blob.Index))
|
||||
res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", body.BlobKZGCommitments[i].String()))
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
@@ -1046,15 +1300,28 @@ func bitvectorToString(input bitfield.Bitvector512) string {
|
||||
return res.String()
|
||||
}
|
||||
|
||||
func attestingIndices(input bitfield.Bitlist, indices []phase0.ValidatorIndex) string {
|
||||
func attestingIndices(input bitfield.Bitlist,
|
||||
committees map[phase0.CommitteeIndex][]phase0.ValidatorIndex,
|
||||
includedCommittees []int,
|
||||
) string {
|
||||
bits := int(input.Len())
|
||||
res := ""
|
||||
|
||||
// Build up the validator list from the included committees.
|
||||
validatorIndices := make([]phase0.ValidatorIndex, 0)
|
||||
for _, committeeIndex := range includedCommittees {
|
||||
validatorIndices = append(validatorIndices, committees[phase0.CommitteeIndex(committeeIndex)]...)
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
|
||||
for i := range bits {
|
||||
if input.BitAt(uint64(i)) {
|
||||
res = fmt.Sprintf("%s%d ", res, indices[i])
|
||||
// Work out the committee and offset given the index.
|
||||
res.WriteString(fmt.Sprintf("%d ", validatorIndices[i]))
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(res)
|
||||
|
||||
return strings.TrimSpace(res.String())
|
||||
}
|
||||
|
||||
func blockGraffiti(_ context.Context, graffiti []byte) string {
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
"github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/deneb"
|
||||
"github.com/attestantio/go-eth2-client/spec/electra"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
@@ -56,18 +57,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
eth2Client: data.eth2Client,
|
||||
}
|
||||
|
||||
specResponse, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
err := populateResults(ctx, results)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
|
||||
return nil, err
|
||||
}
|
||||
genesisResponse, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
|
||||
}
|
||||
genesis := genesisResponse.Data
|
||||
results.genesisTime = genesis.GenesisTime
|
||||
results.slotDuration = specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
|
||||
results.slotsPerEpoch = specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
if data.blockTime != "" {
|
||||
data.blockID, err = timeToBlockID(ctx, data.eth2Client, data.blockTime)
|
||||
@@ -76,64 +69,33 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
}
|
||||
}
|
||||
|
||||
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
block, err := obtainBlock(ctx, data, results)
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
||||
if data.quiet {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil, errors.New("empty beacon block")
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon block")
|
||||
return nil, err
|
||||
}
|
||||
block := blockResponse.Data
|
||||
if data.quiet {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
switch block.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
if err := outputPhase0Block(ctx, data.jsonOutput, block.Phase0); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
err = outputPhase0Block(ctx, data.jsonOutput, block.Phase0)
|
||||
case spec.DataVersionAltair:
|
||||
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, block.Altair); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
err = outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, block.Altair)
|
||||
case spec.DataVersionBellatrix:
|
||||
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, block.Bellatrix); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
err = outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, block.Bellatrix)
|
||||
case spec.DataVersionCapella:
|
||||
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, block.Capella); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
err = outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, block.Capella)
|
||||
case spec.DataVersionDeneb:
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
kzgCommitments, err := block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain blob sidecars")
|
||||
}
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
err = processDenebBlock(ctx, data, block)
|
||||
case spec.DataVersionElectra:
|
||||
err = processElectraBlock(ctx, data, block)
|
||||
default:
|
||||
return nil, errors.New("unknown block version")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to process block")
|
||||
}
|
||||
|
||||
if data.stream {
|
||||
jsonOutput = data.jsonOutput
|
||||
@@ -151,6 +113,97 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return &dataOut{}, nil
|
||||
}
|
||||
|
||||
func populateResults(ctx context.Context, results *dataOut) error {
|
||||
specResponse, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to obtain configuration information")
|
||||
}
|
||||
genesisResponse, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to obtain genesis information")
|
||||
}
|
||||
genesis := genesisResponse.Data
|
||||
results.genesisTime = genesis.GenesisTime
|
||||
results.slotDuration = specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
|
||||
results.slotsPerEpoch = specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func obtainBlock(ctx context.Context, data *dataIn, results *dataOut,
|
||||
) (
|
||||
*spec.VersionedSignedBeaconBlock,
|
||||
error,
|
||||
) {
|
||||
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
||||
if data.quiet {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil, errors.New("empty beacon block")
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon block")
|
||||
}
|
||||
|
||||
return blockResponse.Data, nil
|
||||
}
|
||||
|
||||
func processDenebBlock(ctx context.Context,
|
||||
data *dataIn,
|
||||
block *spec.VersionedSignedBeaconBlock,
|
||||
) error {
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
kzgCommitments, err := block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain blob sidecars")
|
||||
}
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
|
||||
return errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processElectraBlock(ctx context.Context,
|
||||
data *dataIn,
|
||||
block *spec.VersionedSignedBeaconBlock,
|
||||
) error {
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
kzgCommitments, err := block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain blob sidecars")
|
||||
}
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
if err := outputElectraBlock(ctx, data.jsonOutput, data.sszOutput, block.Electra, blobSidecars); err != nil {
|
||||
return errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func headEventHandler(event *apiv1.Event) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -338,6 +391,35 @@ func outputDenebBlock(ctx context.Context,
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputElectraBlock(ctx context.Context,
|
||||
jsonOutput bool,
|
||||
sszOutput bool,
|
||||
signedBlock *electra.SignedBeaconBlock,
|
||||
blobs []*deneb.BlobSidecar,
|
||||
) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
case sszOutput:
|
||||
data, err := signedBlock.MarshalSSZ()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate SSZ")
|
||||
}
|
||||
fmt.Printf("%x\n", data)
|
||||
default:
|
||||
data, err := outputElectraBlockText(ctx, results, signedBlock, blobs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func timeToBlockID(ctx context.Context, eth2Client eth2client.Service, input string) (string, error) {
|
||||
var timestamp time.Time
|
||||
|
||||
|
||||
@@ -97,6 +97,9 @@ func (c *command) process(ctx context.Context) error {
|
||||
case spec.DataVersionDeneb:
|
||||
c.incumbent = state.Deneb.ETH1Data
|
||||
c.eth1DataVotes = state.Deneb.ETH1DataVotes
|
||||
case spec.DataVersionElectra:
|
||||
c.incumbent = state.Electra.ETH1Data
|
||||
c.eth1DataVotes = state.Electra.ETH1DataVotes
|
||||
default:
|
||||
return fmt.Errorf("unhandled beacon state version %v", state.Version)
|
||||
}
|
||||
|
||||
@@ -239,17 +239,21 @@ func (c *command) processSlots(ctx context.Context,
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
for _, attestation := range attestations {
|
||||
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
if attestationData.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestationData.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
|
||||
// Outside of this epoch's range.
|
||||
continue
|
||||
}
|
||||
slotCommittees, exists := allCommittees[attestation.Data.Slot]
|
||||
slotCommittees, exists := allCommittees[attestationData.Slot]
|
||||
if !exists {
|
||||
response, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{
|
||||
State: fmt.Sprintf("%d", attestation.Data.Slot),
|
||||
State: fmt.Sprintf("%d", attestationData.Slot),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
|
||||
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestationData.Slot))
|
||||
}
|
||||
for _, beaconCommittee := range response.Data {
|
||||
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
|
||||
@@ -275,11 +279,11 @@ func (c *command) processSlots(ctx context.Context,
|
||||
}
|
||||
}
|
||||
}
|
||||
slotCommittees = allCommittees[attestation.Data.Slot]
|
||||
slotCommittees = allCommittees[attestationData.Slot]
|
||||
}
|
||||
committee := slotCommittees[attestation.Data.Index]
|
||||
committee := slotCommittees[attestationData.Index]
|
||||
|
||||
inclusionDistance := slot - attestation.Data.Slot
|
||||
inclusionDistance := slot - attestationData.Slot
|
||||
|
||||
head, err := util.AttestationHead(ctx, headersCache, attestation)
|
||||
if err != nil {
|
||||
@@ -298,8 +302,12 @@ func (c *command) processSlots(ctx context.Context,
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
for i := range attestation.AggregationBits.Len() {
|
||||
if attestation.AggregationBits.BitAt(i) {
|
||||
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 {
|
||||
@@ -310,9 +318,9 @@ func (c *command) processSlots(ctx context.Context,
|
||||
|
||||
// Only set the information from the first attestation we find for this validator.
|
||||
if participations[validatorIndex].InclusionSlot == 0 {
|
||||
participations[validatorIndex].HeadVote = &attestation.Data.BeaconBlockRoot
|
||||
participations[validatorIndex].HeadVote = &attestationData.BeaconBlockRoot
|
||||
participations[validatorIndex].Head = &head
|
||||
participations[validatorIndex].TargetVote = &attestation.Data.Target.Root
|
||||
participations[validatorIndex].TargetVote = &attestationData.Target.Root
|
||||
participations[validatorIndex].Target = &target
|
||||
participations[validatorIndex].InclusionSlot = slot
|
||||
}
|
||||
@@ -501,6 +509,8 @@ func (c *command) processBlobs(ctx context.Context) error {
|
||||
// No blobs in these forks.
|
||||
case spec.DataVersionDeneb:
|
||||
c.summary.Blobs += len(block.Deneb.Message.Body.BlobKZGCommitments)
|
||||
case spec.DataVersionElectra:
|
||||
c.summary.Blobs += len(block.Electra.Message.Body.BlobKZGCommitments)
|
||||
default:
|
||||
return fmt.Errorf("unhandled block version %v", block.Version)
|
||||
}
|
||||
|
||||
@@ -120,6 +120,13 @@ func (c *command) process(ctx context.Context) error {
|
||||
} else {
|
||||
c.inclusions = append(c.inclusions, 2)
|
||||
}
|
||||
case spec.DataVersionElectra:
|
||||
aggregate = block.Electra.Message.Body.SyncAggregate
|
||||
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
|
||||
c.inclusions = append(c.inclusions, 1)
|
||||
} else {
|
||||
c.inclusions = append(c.inclusions, 2)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unhandled block version %v", block.Version)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ package depositdata
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -29,6 +31,7 @@ import (
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
debug bool
|
||||
format string
|
||||
timeout time.Duration
|
||||
withdrawalAccount string
|
||||
@@ -39,13 +42,16 @@ type dataIn struct {
|
||||
forkVersion *spec.Version
|
||||
domain *spec.Domain
|
||||
passphrases []string
|
||||
compounding bool
|
||||
}
|
||||
|
||||
func input() (*dataIn, error) {
|
||||
var err error
|
||||
data := &dataIn{
|
||||
debug: viper.GetBool("debug"),
|
||||
forkVersion: &spec.Version{},
|
||||
domain: &spec.Domain{},
|
||||
compounding: viper.GetBool("compounding"),
|
||||
}
|
||||
|
||||
if viper.GetString("validatoraccount") == "" {
|
||||
@@ -97,6 +103,9 @@ func input() (*dataIn, error) {
|
||||
if withdrawalDetailsPresent > 1 {
|
||||
return nil, errors.New("only one of withdrawal account, public key or address is allowed")
|
||||
}
|
||||
if data.compounding && data.withdrawalAddress == "" {
|
||||
return nil, errors.New("a compounding validator must be created with a withdrawal address")
|
||||
}
|
||||
|
||||
if viper.GetString("depositvalue") == "" {
|
||||
return nil, errors.New("deposit value is required")
|
||||
@@ -106,10 +115,6 @@ func input() (*dataIn, error) {
|
||||
return nil, errors.Wrap(err, "deposit value is invalid")
|
||||
}
|
||||
data.amount = spec.Gwei(amount)
|
||||
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
|
||||
if data.amount < 1000000000 { // MIN_DEPOSIT_AMOUNT
|
||||
return nil, errors.New("deposit value must be at least 1 Ether")
|
||||
}
|
||||
|
||||
data.forkVersion, err = inputForkVersion(ctx)
|
||||
if err != nil {
|
||||
@@ -117,6 +122,12 @@ func input() (*dataIn, error) {
|
||||
}
|
||||
|
||||
copy(data.domain[:], e2types.Domain(e2types.DomainDeposit, data.forkVersion[:], e2types.ZeroGenesisValidatorsRoot))
|
||||
if data.debug {
|
||||
fmt.Fprintf(os.Stderr, "Deposit domain is %#x\n", e2types.DomainDeposit)
|
||||
fmt.Fprintf(os.Stderr, "Fork version is %#x\n", *data.forkVersion)
|
||||
fmt.Fprintf(os.Stderr, "Genesis validators root is %#x\n", e2types.ZeroGenesisValidatorsRoot)
|
||||
fmt.Fprintf(os.Stderr, "Signature domain is %#x\n", *data.domain)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -190,17 +190,6 @@ func TestInput(t *testing.T) {
|
||||
},
|
||||
err: "deposit value is required",
|
||||
},
|
||||
{
|
||||
name: "DepositValueTooSmall",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "10s",
|
||||
"validatoraccount": "Test/Interop 0",
|
||||
"withdrawalaccount": "Test/Interop 0",
|
||||
"depositvalue": "1000 Wei",
|
||||
"forkversion": "0x01020304",
|
||||
},
|
||||
err: "deposit value must be at least 1 Ether",
|
||||
},
|
||||
{
|
||||
name: "DepositValueInvalid",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
type dataOut struct {
|
||||
format string
|
||||
account string
|
||||
path string
|
||||
validatorPubKey *spec.BLSPubKey
|
||||
withdrawalCredentials []byte
|
||||
amount spec.Gwei
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
@@ -40,6 +42,21 @@ func process(data *dataIn) ([]*dataOut, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// These values are hard-coded, to allow deposit data to be generated without a connection to the beacon node.
|
||||
if data.amount < 1000000000 { // MIN_DEPOSIT_AMOUNT
|
||||
return nil, errors.New("deposit value must be at least 1 Ether")
|
||||
}
|
||||
switch data.compounding {
|
||||
case false:
|
||||
if data.amount > 32000000000 {
|
||||
return nil, errors.New("deposit value exceeds maximum for a non-compounding validator")
|
||||
}
|
||||
case true:
|
||||
if data.amount > 2048000000000 {
|
||||
return nil, errors.New("deposit value exceeds maximum for a compounding validator")
|
||||
}
|
||||
}
|
||||
|
||||
for _, validatorAccount := range data.validatorAccounts {
|
||||
validatorPubKey, err := ethdoutil.BestPublicKey(validatorAccount)
|
||||
if err != nil {
|
||||
@@ -80,7 +97,7 @@ func process(data *dataIn) ([]*dataOut, error) {
|
||||
copy(depositDataRoot[:], root[:])
|
||||
|
||||
validatorWallet := validatorAccount.(e2wtypes.AccountWalletProvider).Wallet()
|
||||
results = append(results, &dataOut{
|
||||
result := &dataOut{
|
||||
format: data.format,
|
||||
account: fmt.Sprintf("%s/%s", validatorWallet.Name(), validatorAccount.Name()),
|
||||
validatorPubKey: &pubKey,
|
||||
@@ -90,8 +107,53 @@ func process(data *dataIn) ([]*dataOut, error) {
|
||||
forkVersion: data.forkVersion,
|
||||
depositMessageRoot: &depositMessageRoot,
|
||||
depositDataRoot: &depositDataRoot,
|
||||
}
|
||||
if pathProvider, isPathProvider := validatorAccount.(e2wtypes.AccountPathProvider); isPathProvider {
|
||||
result.path = pathProvider.Path()
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Order the results
|
||||
if results[0].path != "" {
|
||||
// Order accounts by their path components.
|
||||
sort.Slice(results, func(i int, j int) bool {
|
||||
iBits := strings.Split(results[i].path, "/")
|
||||
jBits := strings.Split(results[j].path, "/")
|
||||
for index := range iBits {
|
||||
if iBits[index] == "m" && jBits[index] == "m" {
|
||||
continue
|
||||
}
|
||||
if len(jBits) <= index {
|
||||
return false
|
||||
}
|
||||
iBit, err := strconv.ParseUint(iBits[index], 10, 64)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
jBit, err := strconv.ParseUint(jBits[index], 10, 64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if iBit < jBit {
|
||||
return true
|
||||
}
|
||||
if iBit > jBit {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(jBits) > len(iBits)
|
||||
})
|
||||
} else {
|
||||
// Order accounts by their name.
|
||||
sort.Slice(results, func(i int, j int) bool {
|
||||
return strings.Compare(results[i].account, results[j].account) < 0
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -145,7 +207,11 @@ func createWithdrawalCredentials(data *dataIn) ([]byte, error) {
|
||||
withdrawalCredentials = make([]byte, 32)
|
||||
copy(withdrawalCredentials[12:32], withdrawalAddressBytes)
|
||||
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
|
||||
withdrawalCredentials[0] = byte(1) // ETH1_ADDRESS_WITHDRAWAL_PREFIX
|
||||
if data.compounding {
|
||||
withdrawalCredentials[0] = byte(2) // COMPOUNDING_WITHDRAWAL_PREFIX
|
||||
} else {
|
||||
withdrawalCredentials[0] = byte(1) // ETH1_ADDRESS_WITHDRAWAL_PREFIX
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("withdrawal account, public key or address is required")
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
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)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,16 +243,24 @@ func (c *command) processAttesterDutiesSlot(ctx context.Context,
|
||||
return err
|
||||
}
|
||||
for _, attestation := range attestations {
|
||||
if _, exists := dutiesBySlot[attestation.Data.Slot]; !exists {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
if _, exists := dutiesBySlot[attestationData.Slot]; !exists {
|
||||
// We do not have any attestations for this slot.
|
||||
continue
|
||||
}
|
||||
if _, exists := dutiesBySlot[attestation.Data.Slot][attestation.Data.Index]; !exists {
|
||||
if _, exists := dutiesBySlot[attestationData.Slot][attestationData.Index]; !exists {
|
||||
// We do not have any attestations for this committee.
|
||||
continue
|
||||
}
|
||||
for _, duty := range dutiesBySlot[attestation.Data.Slot][attestation.Data.Index] {
|
||||
if attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
||||
for _, duty := range dutiesBySlot[attestationData.Slot][attestationData.Index] {
|
||||
aggregationBits, err := attestation.AggregationBits()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain aggregation bits")
|
||||
}
|
||||
if aggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
||||
// Found it.
|
||||
if _, exists := votes[duty.ValidatorIndex]; exists {
|
||||
// Duplicate; ignore.
|
||||
@@ -261,13 +269,13 @@ func (c *command) processAttesterDutiesSlot(ctx context.Context,
|
||||
votes[duty.ValidatorIndex] = struct{}{}
|
||||
|
||||
// Update the metrics for the attestation.
|
||||
index := int(attestation.Data.Slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
|
||||
index := int(attestationData.Slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
|
||||
c.summary.Slots[index].Attestations.Included++
|
||||
inclusionDelay := slot - duty.Slot
|
||||
|
||||
fault := &validatorFault{
|
||||
Validator: duty.ValidatorIndex,
|
||||
AttestationData: attestation.Data,
|
||||
AttestationData: attestationData,
|
||||
InclusionDistance: int(inclusionDelay),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019 - 2022 Weald Technology Trading
|
||||
// Copyright © 2019 - 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
|
||||
@@ -57,6 +57,7 @@ func init() {
|
||||
validatorDepositDataCmd.Flags().Bool("raw", false, "Print raw deposit data transaction data")
|
||||
validatorDepositDataCmd.Flags().String("forkversion", "", "Use a hard-coded fork version (default is to use mainnet value)")
|
||||
validatorDepositDataCmd.Flags().Bool("launchpad", false, "Print launchpad-compatible JSON")
|
||||
validatorDepositDataCmd.Flags().Bool("compounding", false, "Create a compounding (max 2048 ETH) validator")
|
||||
}
|
||||
|
||||
func validatorDepositdataBindings(cmd *cobra.Command) {
|
||||
@@ -84,4 +85,7 @@ func validatorDepositdataBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("launchpad", cmd.Flags().Lookup("launchpad")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("compounding", cmd.Flags().Lookup("compounding")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2020 - 2022 Weald Technology Trading
|
||||
// Copyright © 2020 - 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
|
||||
@@ -67,9 +67,17 @@ In quiet mode this will return 0 if the validator information can be obtained, o
|
||||
os.Exit(_exitFailure)
|
||||
}
|
||||
|
||||
validator, err := util.ParseValidator(ctx, eth2Client.(eth2client.ValidatorsProvider), viper.GetString("validator"), "head")
|
||||
validator, err := util.ParseValidator(ctx, eth2Client.(eth2client.ValidatorsProvider), viper.GetString("validator"), viper.GetString("blockid"))
|
||||
errCheck(err, "Failed to obtain validator")
|
||||
|
||||
if viper.GetBool("json") {
|
||||
data, err := json.Marshal(validator)
|
||||
errCheck(err, "failed to marshal JSON")
|
||||
fmt.Fprintf(os.Stdout, "%s", string(data))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if viper.GetBool("verbose") {
|
||||
network, err := util.Network(ctx, eth2Client)
|
||||
errCheck(err, "Failed to obtain network")
|
||||
@@ -185,7 +193,8 @@ func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error
|
||||
|
||||
func init() {
|
||||
validatorCmd.AddCommand(validatorInfoCmd)
|
||||
validatorInfoCmd.Flags().String("validator", "", "Public key for which to obtain status")
|
||||
validatorInfoCmd.Flags().String("validator", "", "ID of the validator")
|
||||
validatorInfoCmd.Flags().String("blockid", "head", "the block at which to fetch the information")
|
||||
validatorFlags(validatorInfoCmd)
|
||||
}
|
||||
|
||||
@@ -193,4 +202,7 @@ func validatorInfoBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019 - 2024 Weald Technology Trading.
|
||||
// Copyright © 2019 - 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
|
||||
@@ -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.36.5)"
|
||||
var ReleaseVersion = "local build (latest release 1.37.0)"
|
||||
|
||||
// versionCmd represents the version command.
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
@@ -659,6 +659,7 @@ $ ethdo validator credentials set --validator=Validators/1 --withdrawal-address=
|
||||
- `withdrawaladdress` specify the Ethereum execution address to be used for the withdrawal credentials (if withdrawalpubkey is not supplied)
|
||||
- `withdrawalpubkey` specify the public key to be used for the withdrawal credentials (if withdrawalaccount is not supplied)
|
||||
- `validatoraccount` specify the account to be used for the validator
|
||||
- `compounding` specify if this validator should be compounding, retaining Ether above 32 ETH by default to compound rewards
|
||||
- `depositvalue` specify the amount of the deposit
|
||||
- `forkversion` specify the fork version for the deposit signature; this defaults to mainnet. Note that supplying an incorrect value could result in the loss of your deposit, so only supply this value if you are sure you know what you are doing. You can find the value for other chains by fetching the value supplied in "Genesis fork version" of the `ethdo chain info` command
|
||||
- `raw` generate raw hex output that can be supplied as the data to an Ethereum 1 deposit transaction
|
||||
@@ -682,6 +683,7 @@ $ ethdo validator exit --private-key=0x01e748d098d3bcb477d636f19d510399ae18205fa
|
||||
`ethdo validator info` provides information for a given validator. Options include:
|
||||
|
||||
- `validator`: the validator for which to obtain information, as a [validator specifier](https://github.com/wealdtech/ethdo#validator-specifier)
|
||||
- `blockid`: the ID (slot, root, 'head') of the block at which to obtain information
|
||||
|
||||
```sh
|
||||
$ ethdo validator info --validator=Validators/1
|
||||
|
||||
8
go.mod
8
go.mod
@@ -5,7 +5,7 @@ go 1.22.7
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/attestantio/go-eth2-client v0.22.0
|
||||
github.com/attestantio/go-eth2-client v0.24.0
|
||||
github.com/ferranbt/fastssz v0.1.4
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -62,7 +62,7 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/highwayhash v1.0.3 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
@@ -92,11 +92,11 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb // indirect
|
||||
google.golang.org/grpc v1.69.2 // indirect
|
||||
|
||||
15
go.sum
15
go.sum
@@ -1,5 +1,5 @@
|
||||
github.com/attestantio/go-eth2-client v0.22.0 h1:KmF9kPNNWWGfE7l1BP7pXps4EOXgKnYeFGR0/WbyFhY=
|
||||
github.com/attestantio/go-eth2-client v0.22.0/go.mod h1:d7ZPNrMX8jLfIgML5u7QZxFo2AukLM+5m08iMaLdqb8=
|
||||
github.com/attestantio/go-eth2-client v0.24.0 h1:lGVbcnhlBwRglt1Zs56JOCgXVyLWKFZOmZN8jKhE7Ws=
|
||||
github.com/attestantio/go-eth2-client v0.24.0/go.mod h1:/KTLN3WuH1xrJL7ZZrpBoWM1xCCihnFbzequD5L+83o=
|
||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -76,8 +76,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -213,8 +214,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -229,8 +230,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
|
||||
@@ -62,4 +62,6 @@ type Service interface {
|
||||
CapellaInitialEpoch() phase0.Epoch
|
||||
// DenebInitialEpoch provides the epoch at which the Deneb hard fork takes place.
|
||||
DenebInitialEpoch() phase0.Epoch
|
||||
// ElectraInitialEpoch provides the epoch at which the Electra hard fork takes place.
|
||||
ElectraInitialEpoch() phase0.Epoch
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ type Service struct {
|
||||
bellatrixForkEpoch phase0.Epoch
|
||||
capellaForkEpoch phase0.Epoch
|
||||
denebForkEpoch phase0.Epoch
|
||||
electraForkEpoch phase0.Epoch
|
||||
}
|
||||
|
||||
// module-wide log.
|
||||
@@ -116,6 +117,13 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
|
||||
}
|
||||
log.Trace().Uint64("epoch", uint64(denebForkEpoch)).Msg("Obtained Deneb fork epoch")
|
||||
|
||||
electraForkEpoch, err := fetchElectraForkEpoch(ctx, parameters.specProvider)
|
||||
if err != nil {
|
||||
// Set to far future epoch.
|
||||
electraForkEpoch = 0xffffffffffffffff
|
||||
}
|
||||
log.Trace().Uint64("epoch", uint64(electraForkEpoch)).Msg("Obtained Electra fork epoch")
|
||||
|
||||
s := &Service{
|
||||
genesisTime: genesisResponse.Data.GenesisTime,
|
||||
slotDuration: slotDuration,
|
||||
@@ -125,6 +133,7 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
|
||||
bellatrixForkEpoch: bellatrixForkEpoch,
|
||||
capellaForkEpoch: capellaForkEpoch,
|
||||
denebForkEpoch: denebForkEpoch,
|
||||
electraForkEpoch: electraForkEpoch,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
@@ -341,3 +350,32 @@ func fetchDenebForkEpoch(ctx context.Context,
|
||||
|
||||
return phase0.Epoch(epoch), nil
|
||||
}
|
||||
|
||||
// ElectraInitialEpoch provides the epoch at which the Electra hard fork takes place.
|
||||
func (s *Service) ElectraInitialEpoch() phase0.Epoch {
|
||||
return s.electraForkEpoch
|
||||
}
|
||||
|
||||
func fetchElectraForkEpoch(ctx context.Context,
|
||||
specProvider eth2client.SpecProvider,
|
||||
) (
|
||||
phase0.Epoch,
|
||||
error,
|
||||
) {
|
||||
// Fetch the fork version.
|
||||
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
tmp, exists := specResponse.Data["ELECTRA_FORK_EPOCH"]
|
||||
if !exists {
|
||||
return 0, errors.New("deneb fork version not known by chain")
|
||||
}
|
||||
epoch, isEpoch := tmp.(uint64)
|
||||
if !isEpoch {
|
||||
//nolint:revive
|
||||
return 0, errors.New("ELECTRA_FORK_EPOCH is not a uint64!")
|
||||
}
|
||||
|
||||
return phase0.Epoch(epoch), nil
|
||||
}
|
||||
|
||||
@@ -18,22 +18,41 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/mock"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
"github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/testing/mock"
|
||||
)
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
genesisTime := time.Now()
|
||||
slotDuration := 12 * time.Second
|
||||
slotsPerEpoch := uint64(32)
|
||||
epochsPerSyncCommitteePeriod := uint64(256)
|
||||
ctx := context.Background()
|
||||
|
||||
mockGenesisProvider := mock.NewGenesisProvider(genesisTime)
|
||||
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
|
||||
mockClient, err := mock.New(ctx)
|
||||
require.NoError(t, err)
|
||||
// genesis is 1 day ago.
|
||||
genesisTime := time.Now().AddDate(0, 0, -1)
|
||||
mockClient.GenesisFunc = func(context.Context, *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
|
||||
return &api.Response[*apiv1.Genesis]{
|
||||
Data: &apiv1.Genesis{
|
||||
GenesisTime: genesisTime,
|
||||
},
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
mockClient.SpecFunc = func(context.Context, *api.SpecOpts) (*api.Response[map[string]any], error) {
|
||||
return &api.Response[map[string]any]{
|
||||
Data: map[string]any{
|
||||
"SECONDS_PER_SLOT": time.Second * 12,
|
||||
"SLOTS_PER_EPOCH": uint64(32),
|
||||
"EPOCHS_PER_SYNC_COMMITTEE_PERIOD": uint64(256),
|
||||
},
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -44,7 +63,7 @@ func TestService(t *testing.T) {
|
||||
name: "GenesisProviderMissing",
|
||||
params: []standard.Parameter{
|
||||
standard.WithLogLevel(zerolog.Disabled),
|
||||
standard.WithSpecProvider(mockSpecProvider),
|
||||
standard.WithSpecProvider(mockClient),
|
||||
},
|
||||
err: "problem with parameters: no genesis provider specified",
|
||||
},
|
||||
@@ -52,7 +71,7 @@ func TestService(t *testing.T) {
|
||||
name: "SpecProviderMissing",
|
||||
params: []standard.Parameter{
|
||||
standard.WithLogLevel(zerolog.Disabled),
|
||||
standard.WithGenesisProvider(mockGenesisProvider),
|
||||
standard.WithGenesisProvider(mockClient),
|
||||
},
|
||||
err: "problem with parameters: no spec provider specified",
|
||||
},
|
||||
@@ -60,8 +79,8 @@ func TestService(t *testing.T) {
|
||||
name: "Good",
|
||||
params: []standard.Parameter{
|
||||
standard.WithLogLevel(zerolog.Disabled),
|
||||
standard.WithGenesisProvider(mockGenesisProvider),
|
||||
standard.WithSpecProvider(mockSpecProvider),
|
||||
standard.WithGenesisProvider(mockClient),
|
||||
standard.WithSpecProvider(mockClient),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -80,7 +99,14 @@ func TestService(t *testing.T) {
|
||||
|
||||
// createService is a helper that creates a mock chaintime service.
|
||||
func createService(genesisTime time.Time) (chaintime.Service, time.Duration, uint64, uint64, []*phase0.Fork, error) {
|
||||
slotDuration := 12 * time.Second
|
||||
ctx := context.Background()
|
||||
|
||||
mockClient, err := mock.New(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, 0, 0, nil, err
|
||||
}
|
||||
|
||||
secondsPerSlot := time.Second * 12
|
||||
slotsPerEpoch := uint64(32)
|
||||
epochsPerSyncCommitteePeriod := uint64(256)
|
||||
forkSchedule := []*phase0.Fork{
|
||||
@@ -96,13 +122,36 @@ func createService(genesisTime time.Time) (chaintime.Service, time.Duration, uin
|
||||
},
|
||||
}
|
||||
|
||||
mockGenesisProvider := mock.NewGenesisProvider(genesisTime)
|
||||
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
|
||||
s, err := standard.New(context.Background(),
|
||||
standard.WithGenesisProvider(mockGenesisProvider),
|
||||
standard.WithSpecProvider(mockSpecProvider),
|
||||
mockClient.GenesisFunc = func(context.Context, *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
|
||||
return &api.Response[*apiv1.Genesis]{
|
||||
Data: &apiv1.Genesis{
|
||||
GenesisTime: genesisTime,
|
||||
},
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
mockClient.SpecFunc = func(context.Context, *api.SpecOpts) (*api.Response[map[string]any], error) {
|
||||
return &api.Response[map[string]any]{
|
||||
Data: map[string]any{
|
||||
"SECONDS_PER_SLOT": secondsPerSlot,
|
||||
"SLOTS_PER_EPOCH": slotsPerEpoch,
|
||||
"EPOCHS_PER_SYNC_COMMITTEE_PERIOD": epochsPerSyncCommitteePeriod,
|
||||
},
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
mockClient.ForkScheduleFunc = func(context.Context, *api.ForkScheduleOpts) (*api.Response[[]*phase0.Fork], error) {
|
||||
return &api.Response[[]*phase0.Fork]{
|
||||
Data: forkSchedule,
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
|
||||
s, err := standard.New(ctx,
|
||||
standard.WithGenesisProvider(mockClient),
|
||||
standard.WithSpecProvider(mockClient),
|
||||
)
|
||||
return s, slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod, forkSchedule, err
|
||||
return s, secondsPerSlot, slotsPerEpoch, epochsPerSyncCommitteePeriod, forkSchedule, err
|
||||
}
|
||||
|
||||
func TestGenesisTime(t *testing.T) {
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
// Copyright © 2021 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 mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
)
|
||||
|
||||
// GenesisProvider is a mock for eth2client.GenesisProvider.
|
||||
type GenesisProvider struct {
|
||||
genesisTime time.Time
|
||||
}
|
||||
|
||||
// NewGenesisProvider returns a mock genesis provider with the provided value.
|
||||
func NewGenesisProvider(genesisTime time.Time) eth2client.GenesisProvider {
|
||||
return &GenesisProvider{
|
||||
genesisTime: genesisTime,
|
||||
}
|
||||
}
|
||||
|
||||
// Genesisis a mock.
|
||||
func (m *GenesisProvider) Genesis(_ context.Context, _ *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
|
||||
return &api.Response[*apiv1.Genesis]{
|
||||
Data: &apiv1.Genesis{
|
||||
GenesisTime: m.genesisTime,
|
||||
},
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SpecProvider is a mock for eth2client.SpecProvider.
|
||||
type SpecProvider struct {
|
||||
spec map[string]any
|
||||
}
|
||||
|
||||
// NewSpecProvider returns a mock spec provider with the provided values.
|
||||
func NewSpecProvider(slotDuration time.Duration,
|
||||
slotsPerEpoch uint64,
|
||||
epochsPerSyncCommitteePeriod uint64,
|
||||
) eth2client.SpecProvider {
|
||||
return &SpecProvider{
|
||||
spec: map[string]any{
|
||||
"SECONDS_PER_SLOT": slotDuration,
|
||||
"SLOTS_PER_EPOCH": slotsPerEpoch,
|
||||
"EPOCHS_PER_SYNC_COMMITTEE_PERIOD": epochsPerSyncCommitteePeriod,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Spec is a mock.
|
||||
func (m *SpecProvider) Spec(_ context.Context, _ *api.SpecOpts) (*api.Response[map[string]any], error) {
|
||||
return &api.Response[map[string]any]{
|
||||
Data: m.spec,
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ForkScheduleProvider is a mock for eth2client.ForkScheduleProvider.
|
||||
type ForkScheduleProvider struct {
|
||||
schedule []*phase0.Fork
|
||||
}
|
||||
|
||||
// NewForkScheduleProvider returns a mock spec provider with the provided values.
|
||||
func NewForkScheduleProvider(schedule []*phase0.Fork) eth2client.ForkScheduleProvider {
|
||||
return &ForkScheduleProvider{
|
||||
schedule: schedule,
|
||||
}
|
||||
}
|
||||
|
||||
// ForkSchedule is a mock.
|
||||
func (m *ForkScheduleProvider) ForkSchedule(_ context.Context, _ *api.ForkScheduleOpts) (*api.Response[[]*phase0.Fork], error) {
|
||||
return &api.Response[[]*phase0.Fork]{
|
||||
Data: m.schedule,
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SlotsPerEpochProvider is a mock for eth2client.SlotsPerEpochProvider.
|
||||
type SlotsPerEpochProvider struct {
|
||||
slotsPerEpoch uint64
|
||||
}
|
||||
|
||||
// NewSlotsPerEpochProvider returns a mock slots per epoch provider with the provided value.
|
||||
func NewSlotsPerEpochProvider(slotsPerEpoch uint64) eth2client.SlotsPerEpochProvider {
|
||||
return &SlotsPerEpochProvider{
|
||||
slotsPerEpoch: slotsPerEpoch,
|
||||
}
|
||||
}
|
||||
|
||||
// SlotsPerEpoch is a mock.
|
||||
func (m *SlotsPerEpochProvider) SlotsPerEpoch(_ context.Context) (uint64, error) {
|
||||
return m.slotsPerEpoch, nil
|
||||
}
|
||||
|
||||
// AttestationsSubmitter is a mock for eth2client.AttestationsSubmitter.
|
||||
type AttestationsSubmitter struct{}
|
||||
|
||||
// NewAttestationSubmitter returns a mock attestations submitter with the provided value.
|
||||
func NewAttestationSubmitter() eth2client.AttestationsSubmitter {
|
||||
return &AttestationsSubmitter{}
|
||||
}
|
||||
|
||||
// SubmitAttestations is a mock.
|
||||
func (m *AttestationsSubmitter) SubmitAttestations(_ context.Context, _ []*phase0.Attestation) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeaconBlockSubmitter is a mock for eth2client.BeaconBlockSubmitter.
|
||||
type BeaconBlockSubmitter struct{}
|
||||
|
||||
// NewBeaconBlockSubmitter returns a mock beacon block submitter with the provided value.
|
||||
func NewBeaconBlockSubmitter() eth2client.BeaconBlockSubmitter {
|
||||
return &BeaconBlockSubmitter{}
|
||||
}
|
||||
|
||||
// SubmitBeaconBlock is a mock.
|
||||
func (m *BeaconBlockSubmitter) SubmitBeaconBlock(_ context.Context, _ *spec.VersionedSignedBeaconBlock) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AggregateAttestationsSubmitter is a mock for eth2client.AggregateAttestationsSubmitter.
|
||||
type AggregateAttestationsSubmitter struct{}
|
||||
|
||||
// NewAggregateAttestationsSubmitter returns a mock aggregate attestation submitter with the provided value.
|
||||
func NewAggregateAttestationsSubmitter() eth2client.AggregateAttestationsSubmitter {
|
||||
return &AggregateAttestationsSubmitter{}
|
||||
}
|
||||
|
||||
// SubmitAggregateAttestations is a mock.
|
||||
func (m *AggregateAttestationsSubmitter) SubmitAggregateAttestations(_ context.Context, _ []*phase0.SignedAggregateAndProof) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeaconCommitteeSubscriptionsSubmitter is a mock for eth2client.BeaconCommitteeSubscriptionsSubmitter.
|
||||
type BeaconCommitteeSubscriptionsSubmitter struct{}
|
||||
|
||||
// NewBeaconCommitteeSubscriptionsSubmitter returns a mock beacon committee subscription submitter with the provided value.
|
||||
func NewBeaconCommitteeSubscriptionsSubmitter() eth2client.BeaconCommitteeSubscriptionsSubmitter {
|
||||
return &BeaconCommitteeSubscriptionsSubmitter{}
|
||||
}
|
||||
|
||||
// SubmitBeaconCommitteeSubscriptions is a mock.
|
||||
func (m *BeaconCommitteeSubscriptionsSubmitter) SubmitBeaconCommitteeSubscriptions(_ context.Context, _ []*apiv1.BeaconCommitteeSubscription) error {
|
||||
return nil
|
||||
}
|
||||
@@ -17,19 +17,26 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
)
|
||||
|
||||
// AttestationHead returns the head for which the attestation should have voted.
|
||||
func AttestationHead(ctx context.Context,
|
||||
headersCache *BeaconBlockHeaderCache,
|
||||
attestation *phase0.Attestation,
|
||||
attestation *spec.VersionedAttestation,
|
||||
) (
|
||||
phase0.Root,
|
||||
error,
|
||||
) {
|
||||
slot := attestation.Data.Slot
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return phase0.Root{}, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
slot := attestationData.Slot
|
||||
for {
|
||||
header, err := headersCache.Fetch(ctx, slot)
|
||||
if err != nil {
|
||||
@@ -53,12 +60,17 @@ func AttestationHead(ctx context.Context,
|
||||
// AttestationHeadCorrect returns true if the given attestation had the correct head.
|
||||
func AttestationHeadCorrect(ctx context.Context,
|
||||
headersCache *BeaconBlockHeaderCache,
|
||||
attestation *phase0.Attestation,
|
||||
attestation *spec.VersionedAttestation,
|
||||
) (
|
||||
bool,
|
||||
error,
|
||||
) {
|
||||
slot := attestation.Data.Slot
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
slot := attestationData.Slot
|
||||
for {
|
||||
header, err := headersCache.Fetch(ctx, slot)
|
||||
if err != nil {
|
||||
@@ -74,7 +86,8 @@ func AttestationHeadCorrect(ctx context.Context,
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
|
||||
|
||||
return bytes.Equal(header.Root[:], attestationData.BeaconBlockRoot[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,13 +95,18 @@ func AttestationHeadCorrect(ctx context.Context,
|
||||
func AttestationTarget(ctx context.Context,
|
||||
headersCache *BeaconBlockHeaderCache,
|
||||
chainTime chaintime.Service,
|
||||
attestation *phase0.Attestation,
|
||||
attestation *spec.VersionedAttestation,
|
||||
) (
|
||||
phase0.Root,
|
||||
error,
|
||||
) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return phase0.Root{}, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
// Start with first slot of the target epoch.
|
||||
slot := chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
|
||||
slot := chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
|
||||
for {
|
||||
header, err := headersCache.Fetch(ctx, slot)
|
||||
if err != nil {
|
||||
@@ -113,13 +131,18 @@ func AttestationTarget(ctx context.Context,
|
||||
func AttestationTargetCorrect(ctx context.Context,
|
||||
headersCache *BeaconBlockHeaderCache,
|
||||
chainTime chaintime.Service,
|
||||
attestation *phase0.Attestation,
|
||||
attestation *spec.VersionedAttestation,
|
||||
) (
|
||||
bool,
|
||||
error,
|
||||
) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
// Start with first slot of the target epoch.
|
||||
slot := chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
|
||||
slot := chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
|
||||
for {
|
||||
header, err := headersCache.Fetch(ctx, slot)
|
||||
if err != nil {
|
||||
@@ -135,6 +158,7 @@ func AttestationTargetCorrect(ctx context.Context,
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
|
||||
|
||||
return bytes.Equal(header.Root[:], attestationData.Target.Root[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,28 +18,44 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/mock"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/testing/mock"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func TestParseEpoch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mockClient, err := mock.New(ctx)
|
||||
require.NoError(t, err)
|
||||
// genesis is 1 day ago.
|
||||
genesisTime := time.Now().AddDate(0, 0, -1)
|
||||
slotDuration := 12 * time.Second
|
||||
slotsPerEpoch := uint64(32)
|
||||
epochsPerSyncCommitteePeriod := uint64(256)
|
||||
mockGenesisProvider := mock.NewGenesisProvider(genesisTime)
|
||||
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
|
||||
mockClient.GenesisFunc = func(context.Context, *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
|
||||
return &api.Response[*apiv1.Genesis]{
|
||||
Data: &apiv1.Genesis{
|
||||
GenesisTime: genesisTime,
|
||||
},
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
mockClient.SpecFunc = func(context.Context, *api.SpecOpts) (*api.Response[map[string]any], error) {
|
||||
return &api.Response[map[string]any]{
|
||||
Data: map[string]any{
|
||||
"SECONDS_PER_SLOT": time.Second * 12,
|
||||
"SLOTS_PER_EPOCH": uint64(32),
|
||||
},
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
chainTime, err := standardchaintime.New(context.Background(),
|
||||
standardchaintime.WithLogLevel(zerolog.Disabled),
|
||||
standardchaintime.WithGenesisProvider(mockGenesisProvider),
|
||||
standardchaintime.WithSpecProvider(mockSpecProvider),
|
||||
standardchaintime.WithGenesisProvider(mockClient),
|
||||
standardchaintime.WithSpecProvider(mockClient),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -18,28 +18,44 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/mock"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/testing/mock"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func TestParseSlot(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mockClient, err := mock.New(ctx)
|
||||
require.NoError(t, err)
|
||||
// genesis is 1 day ago.
|
||||
genesisTime := time.Now().AddDate(0, 0, -1)
|
||||
slotDuration := 12 * time.Second
|
||||
slotsPerSlot := uint64(32)
|
||||
epochsPerSyncCommitteePeriod := uint64(256)
|
||||
mockGenesisProvider := mock.NewGenesisProvider(genesisTime)
|
||||
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerSlot, epochsPerSyncCommitteePeriod)
|
||||
mockClient.GenesisFunc = func(context.Context, *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
|
||||
return &api.Response[*apiv1.Genesis]{
|
||||
Data: &apiv1.Genesis{
|
||||
GenesisTime: genesisTime,
|
||||
},
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
mockClient.SpecFunc = func(context.Context, *api.SpecOpts) (*api.Response[map[string]any], error) {
|
||||
return &api.Response[map[string]any]{
|
||||
Data: map[string]any{
|
||||
"SECONDS_PER_SLOT": time.Second * 12,
|
||||
"SLOTS_PER_EPOCH": uint64(32),
|
||||
},
|
||||
Metadata: make(map[string]any),
|
||||
}, nil
|
||||
}
|
||||
chainTime, err := standardchaintime.New(context.Background(),
|
||||
standardchaintime.WithLogLevel(zerolog.Disabled),
|
||||
standardchaintime.WithGenesisProvider(mockGenesisProvider),
|
||||
standardchaintime.WithSpecProvider(mockSpecProvider),
|
||||
standardchaintime.WithGenesisProvider(mockClient),
|
||||
standardchaintime.WithSpecProvider(mockClient),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user