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

Separate type for unaggregated network attestations #14659

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

Conversation

rkapka
Copy link
Contributor

@rkapka rkapka commented Nov 22, 2024

What type of PR is this?

Feature

What does this PR do? Why is it needed?

Implements ethereum/consensus-specs#3900

The above consensus change introduces a new SingleAttestation type for unaggregated attestations. AttestationElectra will still be used for aggregated atts. Using the single version in all of our code would be a huge undertaking because many components, such as the attestation pool and attestation caches, require attestations to be of the old format. Modifying these components to work both with pre-Electra attestations and the single attestation is risky and too much work for Pectra.

The main reason why SingleAttestation was introduced is to prevent a DoS attack, which means it's main value is at the level of gossip. Once we pass gossip validation checks, there is no downside to using the AttestationElectra for unaggregated attestations too. This allows us to leave most attestation-related code untouched. To have the best of both worlds, we convert the single attestation to an Electra attestation as quickly as possible and use the latter from that point on.

When converting from SingleAttestation to ElectraAttestation we need to find the index within the committee of the attester_index from the single attestation, because attester_index is the validator index within the whole state. To give an example:

Validator 9999 is an attester and the committee index to which it is assigned is 5. Within committee number 5, the attester's position/index is 7. SingleAttestation's values will be attester_index = 9999 and committee_index = 5. When converting to an ElectraAttestation, we need to set bit number 7 in attestation_bits. We need to obtain the number 7 somehow as it is not present in the SingleAttestation.

The way it's done is by using the AttestationStateFetcher to get the attestation's target state, then get the appropriate committee from that state and lastly call ToAttestationElectra on the SingleAttestation, passing the committee to the function.

Other notes for review

  • Obtaining the target state, getting the committee and converting takes less than 1ms. I presume it's because both the state and committee are cached.
  • Tested on Kurtosis with Prysm-only nodes and and Prysm+Lodestar.
  • The same "convert quickly to Electra attestation" design is what other clients are doing too to minimize disruption.

Acknowledgements

  • I have read CONTRIBUTING.md.
  • I have made an appropriate entry to CHANGELOG.md.
  • I have added a description to this PR with sufficient context for reviewers to understand this PR.

@rkapka rkapka marked this pull request as ready for review December 10, 2024 22:00
@rkapka rkapka requested a review from a team as a code owner December 10, 2024 22:00
@rkapka rkapka changed the title Single att Separate type for unaggregated network attestations Dec 10, 2024
Comment on lines 97 to 103
att.AggregationBits = bitfield.NewBitlist(1)
committeeIndex, err := att.GetCommitteeIndex()
require.NoError(t, err)
_, result, err = service.validateBitLength(ctx, s, att.Data.Slot, committeeIndex, att.AggregationBits)
require.ErrorContains(t, "wanted participants bitfield length 4, got: 1", err)
assert.Equal(t, pubsub.ValidationReject, result)

Copy link
Member

Choose a reason for hiding this comment

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

Why is this test deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

never mind, I will revert this change

Comment on lines 73 to 98

preState, err := s.cfg.chain.AttestationTargetState(ctx, data.Target)
if err != nil {
tracing.AnnotateError(span, err)
return pubsub.ValidationIgnore, err
}
committeeIndex, err := att.GetCommitteeIndex()
if err != nil {
tracing.AnnotateError(span, err)
return pubsub.ValidationIgnore, err
}
committee, err := helpers.BeaconCommitteeFromState(ctx, preState, att.GetData().Slot, committeeIndex)
if err != nil {
tracing.AnnotateError(span, err)
return pubsub.ValidationIgnore, err
}

var singleAtt *eth.SingleAttestation
if att.Version() >= version.Electra {
singleAtt, ok = att.(*eth.SingleAttestation)
if !ok {
return pubsub.ValidationIgnore, fmt.Errorf("attestation has wrong type (expected %T, got %T)", &eth.SingleAttestation{}, att)
}
att = singleAtt.ToAttestationElectra(committee)
}

Copy link
Member

Choose a reason for hiding this comment

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

Is there a DOS vector for calling AttestationTargetState and BeaconCommitteeFromState this early?

Comment on lines -309 to -318
name: "nil signature",
attestation: &ethpb.AttestationElectra{
AggregationBits: testhelpers.FillByteSlice(4, 74),
Data: &ethpb.AttestationData{
Source: &ethpb.Checkpoint{},
Target: &ethpb.Checkpoint{},
},
CommitteeBits: testhelpers.FillByteSlice(8, 83),
},
expectedErrorMessage: "attestation signature can't be nil",
Copy link
Member

Choose a reason for hiding this comment

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

Why nil signature test removed?

Copy link
Contributor Author

@rkapka rkapka Dec 18, 2024

Choose a reason for hiding this comment

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

I removed it because we had two functions in the codebase checking if an attestation is nil. One is helpers.ValidateNilAttestation and the other one is validateNilAttestation on the VC side. The only difference between them was signature validation which is missing in the helpers package. I decided it's better to just have a single implementation and I expect the one in helpers to be more accurate. I am not exactly sure what happens though when the BN receives an attestation without a signature if the function in helpers doesn't fail (most likely signature validation fails in some other place).

if !ok {
return pubsub.ValidationIgnore, fmt.Errorf("attestation has wrong type (expected %T, got %T)", &eth.SingleAttestation{}, att)
}
att = singleAtt.ToAttestationElectra(committee)
Copy link
Member

Choose a reason for hiding this comment

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

This is the biggest additional performance penalty as far as I can see. For a node that subscribe to several subnets. Is this a concern? We will call this 10-30k times per slot

@@ -71,6 +70,32 @@ func (s *Service) validateCommitteeIndexBeaconAttestation(ctx context.Context, p
if data.Slot == 0 {
return pubsub.ValidationIgnore, nil
}

preState, err := s.cfg.chain.AttestationTargetState(ctx, data.Target)
Copy link
Member

Choose a reason for hiding this comment

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

Why are we calling this here ? There must be strong justification for

  • fetching a state
  • fetching the committee from it

As these are additional bottlenecks on our highest traffic gossip path

Copy link
Member

Choose a reason for hiding this comment

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

Also why is this being called before the current gossip checks


committee, result, err := s.validateBitLength(ctx, bs, aggregate.GetData().Slot, committeeIndex, aggregate.GetAggregationBits())
Copy link
Member

Choose a reason for hiding this comment

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

why was this removed ? Its effectively the same as what has been added in

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you're right, I will change it back

committee, result, err := s.validateBitLength(ctx, bs, a.GetData().Slot, committeeIndex, a.GetAggregationBits())
if result != pubsub.ValidationAccept {
return result, err
committee, err := helpers.BeaconCommitteeFromState(ctx, bs, a.GetData().Slot, committeeIndex)
Copy link
Member

Choose a reason for hiding this comment

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

Same here, why is validateBitLength removed

@@ -115,6 +104,48 @@ func (s *Service) validateCommitteeIndexBeaconAttestation(ctx context.Context, p
}
}

if !s.cfg.chain.InForkchoice(bytesutil.ToBytes32(data.BeaconBlockRoot)) {
Copy link
Member

Choose a reason for hiding this comment

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

Executing all these methods before we have validated data.BeaconBlockRoot is wrong. As we might have not processed the block yet (and it has to be inserted into our pending queue)

return pubsub.ValidationIgnore, blockchain.ErrNotDescendantOfFinalized
}

if err = s.cfg.chain.VerifyLmdFfgConsistency(ctx, att); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Same here and for the rest of the methods that follow it. If it is difficult enough, you should just stream the attestations after

terencechain
terencechain previously approved these changes Dec 22, 2024
@@ -141,23 +162,25 @@ func (s *Service) validateCommitteeIndexBeaconAttestation(ctx context.Context, p
tracing.AnnotateError(span, blockchain.ErrNotDescendantOfFinalized)
return pubsub.ValidationIgnore, blockchain.ErrNotDescendantOfFinalized
}
if err := s.cfg.chain.VerifyLmdFfgConsistency(ctx, att); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

This check is performed now after you are grabbing a full state and computing a beacon committee shuffling, this opens up the door for a malicious peer to kill the node with a crafted attestation.

ctx, span := trace.StartSpan(ctx, "AttesterServer.ProposeAttestationElectra")
defer span.End()

committeeIndex, err := att.GetCommitteeIndex()
targetState, err := vs.AttestationStateFetcher.AttestationTargetState(ctx, att.Data.Target)
Copy link
Member

Choose a reason for hiding this comment

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

Any concerns with this line: for every attestation we propose, we need to retrieve the target state. What are the implications of this?

# Conflicts:
#	CHANGELOG.md
#	beacon-chain/rpc/eth/beacon/handlers_pool.go
#	beacon-chain/rpc/prysm/v1alpha1/validator/attester.go
#	beacon-chain/rpc/prysm/v1alpha1/validator/server.go
#	beacon-chain/rpc/service.go
#	proto/prysm/v1alpha1/attestation.go
log.WithError(err).Error("could not save attestation")
}
} else if att.IsAggregated() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

single attestation can't be aggregated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants