Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decompose Electra block attestations #14896

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open

Conversation

rkapka
Copy link
Contributor

@rkapka rkapka commented Feb 7, 2025

What type of PR is this?

Bug fix

What does this PR do? Why is it needed?

Prysm nodes on Electra devnets experience attestation packing issues. Every block is filled up to the brim with 8 attestations. Except for attestations for the current slot, which should go into the block, it includes redundant attestations from previous slots that have been already included by proposers of older blocks.

The reason why this happens is that we don't delete aggregated attestations from the pool correctly when we receive a block. Redundant packing is mostly a "cosmetic" bug, but not deleting attestations means that we keep much more of them in the pool than necessary, bloating the size of the pool needlessly. And we don't delete them because we can't compare Electra block attestations with aggregated attestations in the same way we compare pre-Electra block attestations. Because in Electra block attestations can combine aggregates for multiple committees, comparing attestations bits of a block attestation with attestation bits of an aggregate can cause unexpected results, leading to covered aggregates not being removed from the pool. To make sure aggregates are removed, we decompose the block attestation into dummy aggregates, with each aggregate accounting for one committee. This allows us to compare aggregates in the same way it's done pre-Electra.

Other notes for review

Another suggested approach was to use the SlotCommitteeCount helper and not creating dummy aggregates, but methods that delete aggregates expect attestations as parameters, so that would require changing how these interfaces work, and that's much more invasive than the current approach.

Acknowledgements

@rkapka rkapka force-pushed the electra-packing-fix branch from dd6aac7 to b5448d6 Compare February 7, 2025 21:29
@james-prysm james-prysm added the Electra electra hardfork label Feb 8, 2025
@rkapka rkapka force-pushed the electra-packing-fix branch 2 times, most recently from 61293df to d47be66 Compare February 14, 2025 10:27
@rkapka rkapka marked this pull request as ready for review February 14, 2025 10:27
@rkapka rkapka requested a review from a team as a code owner February 14, 2025 10:27
@rkapka rkapka added the Bug Something isn't working label Feb 14, 2025
Comment on lines +469 to +466
committeeIndices := att.CommitteeBitsVal().BitIndices()
committees, err := helpers.AttestationCommittees(ctx, headState, att)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check whether committeeIndices and committees have the same length?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at first I thought maybe seconding this but are there some guarantees due to already being from the head state/headblock?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a sanity check

if features.Get().EnableExperimentalAttestationPool {
if err := s.cfg.AttestationCache.DeleteCovered(att); err != nil {
return errors.Wrap(err, "could not delete attestation")
if !att.IsAggregated() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about breaking this out into its own function and using a switch statement instead? it'll read easier than seeing continue several times here

func (s *Service) pruneAttsFromPool(ctx context.Context, headState state.BeaconState, headBlock interfaces.ReadOnlySignedBeaconBlock) error {
    atts := headBlock.Block().Body().Attestations()
    for _, att := range atts {
        if err := s.handleAttestation(ctx, headState, att); err != nil {
            return err
        }
    }
    return nil
}

func (s *Service) handleAttestation(ctx context.Context, headState state.BeaconState, att ethpb.Att) error {
    switch {
    case !att.IsAggregated():
        return s.cfg.AttPool.DeleteUnaggregatedAttestation(att)

    case att.Version() == version.Phase0:
        if features.Get().EnableExperimentalAttestationPool {
            return s.cfg.AttestationCache.DeleteCovered(att)
        }
        return s.cfg.AttPool.DeleteAggregatedAttestation(att)

    default:
        return s.pruneElectraAttsFromPool(ctx, headState, att)
    }
}

return errors.Wrap(err, "could not delete attestation")
if !att.IsAggregated() {
if err := s.cfg.AttPool.DeleteUnaggregatedAttestation(att); err != nil {
return err
Copy link
Contributor

@james-prysm james-prysm Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be helpful to wrap these errors to know if it's unaggregated or based on electra?


for i, c := range committees {
ab := bitfield.NewBitlist(uint64(len(c)))
for j := uint64(0); j < uint64(len(c)); j++ {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is something like the below needed for safety?
if offset+uint64(len(c)) > uint64(att.GetAggregationBits().Len()) {
return errors.New("committee bits exceed available aggregation bits")
}

I think you can also do for j := range len(c), don't have a strong preference for it but it can be useful.

is it possible to have a test for the check I suggested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is something like the below needed for safety?

This can never happen because it means that the attestation bits have wrong lengths, and such a block will get rejected.

I think you can also do for j := range len(c)

This requires j to be an int, and then I will have to do casting two times below.

is it possible to have a test for the check I suggested?

As per above, I don't think it's necessary because it can never happen (unless we have a bug in some other place of the code, which is another story)

@rkapka rkapka force-pushed the electra-packing-fix branch from 025bf00 to 30e5a58 Compare February 18, 2025 16:41
func (s *Service) pruneAttsFromPool(ctx context.Context, headState state.BeaconState, headBlock interfaces.ReadOnlySignedBeaconBlock) error {
for _, att := range headBlock.Block().Body().Attestations() {
if err := s.pruneCoveredAttsFromPool(ctx, headState, att); err != nil {
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why returning here instead of continuing and removing the rest?

offset := uint64(0)

committeeIndices := att.CommitteeBitsVal().BitIndices()
committees, err := helpers.AttestationCommittees(ctx, headState, att)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have several issues/questions with this function

  1. Are we certain these committees will be cached already? I wonder if this is not a DOS when importing an old block that will require to do a computation.
  2. This function is called in a loop for att but the committees are all taken from the same state. Are the successive calls No-ops or if they aren't you should pass the committees as parameters.
  3. Is there a way of knowing if we have the attestation in the pool already before doing any processing on the block ones?

@rkapka rkapka force-pushed the electra-packing-fix branch 3 times, most recently from 3e90fc2 to fc5cf42 Compare February 19, 2025 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Something isn't working Electra electra hardfork
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants