Additional electra support.

This commit is contained in:
Jim McDonald
2024-05-15 00:06:52 +01:00
parent 083f484907
commit e29818340d
11 changed files with 742 additions and 132 deletions

View File

@@ -93,23 +93,33 @@ 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 {
headCorrect, err = calcHeadCorrect(ctx, data, attestation)
headCorrect, err = calcHeadCorrect(ctx, data, attestationData)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain head correct result")
}
targetCorrect, err = calcTargetCorrect(ctx, data, attestation)
targetCorrect, err = calcTargetCorrect(ctx, data, attestationData)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain target correct result")
}
}
results.found = true
results.attestation = attestation
// TODO fix.
// results.attestation = attestation
results.slot = slot
results.attestationIndex = uint64(i)
results.inclusionDelay = slot - duty.Slot
@@ -128,8 +138,8 @@ 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, attestationData *phase0.AttestationData) (bool, error) {
slot := attestationData.Slot
for {
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
@@ -149,13 +159,13 @@ 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, attestationData *phase0.AttestationData) (bool, error) {
// 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 +185,7 @@ 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
}
}

View File

@@ -21,6 +21,7 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec"
"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"
@@ -62,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 {
continue
}
if attestationData.Slot < minSlot {
minSlot = attestationData.Slot
}
}
if c.debug {
@@ -102,71 +107,24 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
if c.debug {
fmt.Printf("Processing attestation %d\n", i)
}
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
root, err := attestation.HashTreeRoot()
if err != nil {
return err
}
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
var analysis *attestationAnalysis
switch attestation.Version {
case spec.DataVersionPhase0:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Phase0, blockVotes, spec.DataVersionPhase0)
case spec.DataVersionAltair:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Altair, blockVotes, spec.DataVersionAltair)
case spec.DataVersionBellatrix:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Bellatrix, blockVotes, spec.DataVersionBellatrix)
case spec.DataVersionCapella:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Capella, blockVotes, spec.DataVersionCapella)
case spec.DataVersionDeneb:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Deneb, blockVotes, spec.DataVersionDeneb)
case spec.DataVersionElectra:
analysis, err = c.electraAnalysis(ctx, slot, attestation.Electra, blockVotes, spec.DataVersionElectra)
default:
return fmt.Errorf("unknown version %s\n", attestation.Version)
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
var err error
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate head timely.
analysis.HeadTimely = analysis.HeadCorrect && attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate target timely.
if block.Version < spec.DataVersionDeneb {
analysis.TargetTimely = attestation.Data.Slot >= slot-32
} else {
analysis.TargetTimely = true
}
}
// Calculate score and value.
@@ -188,6 +146,165 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
return nil
}
func (c *command) phase0Analysis(ctx context.Context,
slot phase0.Slot,
attestation *phase0.Attestation,
blockVotes map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist,
version spec.DataVersion,
) (
*attestationAnalysis,
error,
) {
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
// TODO.
root, err := attestation.HashTreeRoot()
if err != nil {
return nil, err
}
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
if err != nil {
return nil, err
}
// Calculate head timely.
analysis.HeadTimely = analysis.HeadCorrect && attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
if err != nil {
return nil, err
}
// Calculate target timely.
if version < spec.DataVersionDeneb {
analysis.TargetTimely = attestation.Data.Slot >= slot-32
} else {
analysis.TargetTimely = true
}
}
return analysis, nil
}
func (c *command) electraAnalysis(ctx context.Context,
slot phase0.Slot,
attestation *electra.Attestation,
blockVotes map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist,
version spec.DataVersion,
) (
*attestationAnalysis,
error,
) {
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
// TODO.
root, err := attestation.HashTreeRoot()
if err != nil {
return nil, err
}
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
var err error
analysis.HeadCorrect, err = c.calcElectraHeadCorrect(ctx, attestation)
if err != nil {
return nil, err
}
// Calculate head timely.
analysis.HeadTimely = analysis.HeadCorrect && attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcElectraTargetCorrect(ctx, attestation)
if err != nil {
return nil, err
}
// Calculate target timely.
if version < spec.DataVersionDeneb {
analysis.TargetTimely = attestation.Data.Slot >= slot-32
} else {
analysis.TargetTimely = true
}
}
return analysis, nil
}
func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedBeaconBlock, minSlot phase0.Slot) error {
parentRoot, err := block.ParentRoot()
if err != nil {
@@ -237,10 +354,10 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
}
func (c *command) processParentBlock(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
}
// attestations, err := block.Attestations()
// if err != nil {
// return err
// }
slot, err := block.Slot()
if err != nil {
return err
@@ -249,31 +366,40 @@ func (c *command) processParentBlock(_ context.Context, block *spec.VersionedSig
fmt.Printf("Processing block %d\n", slot)
}
for i, attestation := range attestations {
root, err := attestation.HashTreeRoot()
if err != nil {
return err
}
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
Block: slot,
Index: i,
}
// TODO reinstate.
// for i, attestation := range attestations {
// root, err := attestation.HashTreeRoot()
// if err != nil {
// return err
// }
// c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
// Block: slot,
// Index: i,
// }
data := attestation.Data
_, exists := c.votes[data.Slot]
if !exists {
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = c.votes[data.Slot][data.Index]
if !exists {
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
c.votes[data.Slot][data.Index].SetBitAt(j, true)
}
}
}
// data, err := attestation.Data()
// if err != nil {
// return err
// }
// aggregationBits, err := attestation.AggregationBits()
// if err != nil {
// return err
// }
// _, exists := c.votes[data.Slot]
// if !exists {
// c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
// }
// _, exists = c.votes[data.Slot][data.Index]
// if !exists {
// c.votes[data.Slot][data.Index] = bitfield.NewBitlist(aggregationBits.Len())
// }
// for j := uint64(0); j < aggregationBits.Len(); j++ {
// if aggregationBits.BitAt(j) {
// c.votes[data.Slot][data.Index].SetBitAt(j, true)
// }
// }
//}
return nil
}
@@ -458,6 +584,79 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
}
func (c *command) calcElectraHeadCorrect(ctx context.Context, attestation *electra.Attestation) (bool, error) {
slot := attestation.Data.Slot
root, exists := c.headRoots[slot]
if !exists {
for {
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
if c.debug {
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
}
return false, nil
}
return false, err
}
if response.Data == nil {
// No block.
slot--
continue
}
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
c.headRoots[attestation.Data.Slot] = response.Data.Root
root = response.Data.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
func (c *command) calcElectraTargetCorrect(ctx context.Context, attestation *electra.Attestation) (bool, error) {
root, exists := c.targetRoots[attestation.Data.Slot]
if !exists {
// Start with first slot of the target epoch.
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
if c.debug {
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
}
return false, nil
}
}
if response.Data == nil {
// No block.
slot--
continue
}
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
c.targetRoots[attestation.Data.Slot] = response.Data.Root
root = response.Data.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
}
func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
c.analysis.SyncCommitee = &syncCommitteeAnalysis{}
switch block.Version {

View File

@@ -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"
"github.com/prysmaticlabs/go-bitfield"
@@ -204,6 +205,120 @@ func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Ser
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]
}
res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index))
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(" 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 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{}
@@ -517,6 +632,116 @@ func outputDenebBlockText(ctx context.Context,
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.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputElectraBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputElectraBlobInfo(ctx, data.verbose, signedBlock.Message.Body, blobs)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
@@ -903,6 +1128,71 @@ func outputDenebBlockExecutionPayload(_ context.Context,
return res.String(), nil
}
func outputElectraBlockExecutionPayload(_ context.Context,
verbose bool,
payload *electra.ExecutionPayload,
) (
string,
error,
) {
if payload == nil {
return "", nil
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString("Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString("Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
} else {
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString(" Base fee per gas: ")
res.WriteString(string2eth.WeiToString(payload.BaseFeePerGas.ToBig(), true))
res.WriteString("\n Block hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
res.WriteString(" Timestamp: ")
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
res.WriteString(" Prev RANDAO: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
res.WriteString(" Receipts root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
if utf8.Valid(payload.ExtraData) {
res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData)))
} else {
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
}
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
res.WriteString(" Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
res.WriteString(" Withdrawals: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Withdrawals)))
res.WriteString(" Excess blob gas: ")
res.WriteString(fmt.Sprintf("%d\n", payload.ExcessBlobGas))
}
return res.String(), nil
}
func outputDenebBlobInfo(_ context.Context,
verbose bool,
body *deneb.BeaconBlockBody,
@@ -932,6 +1222,35 @@ func outputDenebBlobInfo(_ context.Context,
return res.String(), nil
}
func outputElectraBlobInfo(_ context.Context,
verbose bool,
body *electra.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(" Index: %d\n", blob.Index))
res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", body.BlobKZGCommitments[i].String()))
}
return res.String(), nil
}
func outputBellatrixBlockExecutionPayload(_ context.Context,
verbose bool,
payload *bellatrix.ExecutionPayload,

View File

@@ -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"
@@ -124,6 +125,17 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionElectra:
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 := outputElectraBlock(ctx, data.jsonOutput, data.sszOutput, block.Electra, blobSidecars); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
return nil, errors.New("unknown block version")
}
@@ -187,6 +199,14 @@ func headEventHandler(event *apiv1.Event) {
if err == nil {
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, block.Deneb, blobSidecarsResponse.Data)
}
case spec.DataVersionElectra:
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
Block: blockID,
})
if err == nil {
err = outputElectraBlock(context.Background(), jsonOutput, sszOutput, block.Electra, blobSidecarsResponse.Data)
}
default:
err = errors.New("unknown block version")
}
@@ -319,6 +339,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

View File

@@ -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)
}

View File

@@ -184,17 +184,21 @@ func (c *command) processSlots(ctx context.Context,
return 0, 0, 0, 0, 0, 0, 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 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain attestation data for slot %d", slot))
}
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 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
return 0, 0, 0, 0, 0, 0, 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 {
@@ -209,22 +213,26 @@ 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
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
inclusionDistance := slot - attestationData.Slot
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestationData)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestationData)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
if attestation.AggregationBits.BitAt(i) {
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for i := uint64(0); i < aggregationBits.Len(); i++ {
if aggregationBits.BitAt(i) {
votes[committee[int(i)]] = struct{}{}
if _, exists := headCorrects[committee[int(i)]]; !exists && headCorrect {
headCorrects[committee[int(i)]] = struct{}{}
@@ -245,6 +253,7 @@ func (c *command) processSlots(ctx context.Context,
}
}
}
return len(votes),
len(headCorrects),
len(headTimelys),
@@ -392,6 +401,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)
}

View File

@@ -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)
}

View File

@@ -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 {
continue
}
aggregationBits, err := attestation.AggregationBits()
if err != nil {
continue
}
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] {
if aggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
// Found it.
if _, exists := votes[duty.ValidatorIndex]; exists {
// Duplicate; ignore.
@@ -261,17 +269,17 @@ 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),
}
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestationData)
if err != nil {
return errors.Wrap(err, "failed to calculate if attestation had correct head vote")
}
@@ -295,7 +303,7 @@ func (c *command) processAttesterDutiesSlot(ctx context.Context,
c.summary.UntimelySourceValidators = append(c.summary.UntimelySourceValidators, fault)
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestationData)
if err != nil {
return errors.Wrap(err, "failed to calculate if attestation had correct target vote")
}

4
go.mod
View File

@@ -5,7 +5,7 @@ go 1.21
toolchain go1.21.6
require (
github.com/attestantio/go-eth2-client v0.21.4
github.com/attestantio/go-eth2-client v0.21.4-0.20240514220516-2646ce28c3e4
github.com/ferranbt/fastssz v0.1.3
github.com/gofrs/uuid v4.4.0+incompatible
github.com/google/uuid v1.6.0
@@ -69,7 +69,7 @@ require (
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pk910/dynamic-ssz v0.0.3 // indirect
github.com/pk910/dynamic-ssz v0.0.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect

4
go.sum
View File

@@ -1,3 +1,5 @@
github.com/attestantio/go-eth2-client v0.21.4-0.20240514220516-2646ce28c3e4 h1:tvLDLWLMMdo/UzMPCX+a9wh+zdAZzS2NEOqAaq9m5BY=
github.com/attestantio/go-eth2-client v0.21.4-0.20240514220516-2646ce28c3e4/go.mod h1:RssbJ8txdfZ+O7cIjOKCvizcTWHzgPuJNByrwtQ6tBQ=
github.com/attestantio/go-eth2-client v0.21.4 h1:1QW4f3NXCcbUsxmRBElotTjSIhRwLsmdowUvxJnyaJU=
github.com/attestantio/go-eth2-client v0.21.4/go.mod h1:d7ZPNrMX8jLfIgML5u7QZxFo2AukLM+5m08iMaLdqb8=
github.com/aws/aws-sdk-go v1.51.1 h1:AFvTihcDPanvptoKS09a4yYmNtPm3+pXlk6uYHmZiFk=
@@ -111,6 +113,8 @@ github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOS
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pk910/dynamic-ssz v0.0.3 h1:fCWzFowq9P6SYCc7NtJMkZcIHk+r5hSVD+32zVi6Aio=
github.com/pk910/dynamic-ssz v0.0.3/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c=
github.com/pk910/dynamic-ssz v0.0.4 h1:DT29+1055tCEPCaR4V/ez+MOKW7BzBsmjyFvBRqx0ME=
github.com/pk910/dynamic-ssz v0.0.4/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View File

@@ -24,12 +24,12 @@ import (
// AttestationHeadCorrect returns true if the given attestation had the correct head.
func AttestationHeadCorrect(ctx context.Context,
headersCache *BeaconBlockHeaderCache,
attestation *phase0.Attestation,
attestationData *phase0.AttestationData,
) (
bool,
error,
) {
slot := attestation.Data.Slot
slot := attestationData.Slot
for {
header, err := headersCache.Fetch(ctx, slot)
if err != nil {
@@ -45,7 +45,7 @@ func AttestationHeadCorrect(ctx context.Context,
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
return bytes.Equal(header.Root[:], attestationData.BeaconBlockRoot[:]), nil
}
}
@@ -53,13 +53,13 @@ func AttestationHeadCorrect(ctx context.Context,
func AttestationTargetCorrect(ctx context.Context,
headersCache *BeaconBlockHeaderCache,
chainTime chaintime.Service,
attestation *phase0.Attestation,
attestationData *phase0.AttestationData,
) (
bool,
error,
) {
// 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 {
@@ -75,6 +75,6 @@ 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
}
}