Compare commits

...

2 Commits

Author SHA1 Message Date
terence
5d524bf6ef Add Gloas attestation committee index validation 2026-02-13 19:42:41 -08:00
terence
22e77add54 Refactor function ProcessExecutionPayload with ApplyExecutionPayload (#16356)
This PR refactors `ProcessExecutionPayload` with `ApplyExecutionPayload`
so caller can use Apply method to calculate post state root. Note that
validations are not required for Apply function. We really need the
state mutation lines that's:
```
1. Ensure latest_block_header.state_root is set (if zero, set it to the pre‑payload HashTreeRoot)...
2. processExecutionRequests()
3. QueueBuilderPayment()
4. SetExecutionPayloadAvailability(state.Slot(), true)
5. SetLatestBlockHash(payload.BlockHash())
```
I decided to keep them there because a. it's cheap b. it makes refactor
cleaner to reason c. API/caller may want to validate envelope and bid
consistency (ex: beacon api has option to validate consensus)
2026-02-13 15:51:22 +00:00
5 changed files with 144 additions and 15 deletions

View File

@@ -112,6 +112,34 @@ func ProcessExecutionPayload(
return errors.Wrap(err, "signature verification failed")
}
envelope, err := signedEnvelope.Envelope()
if err != nil {
return errors.Wrap(err, "could not get envelope from signed envelope")
}
if err := ApplyExecutionPayload(ctx, st, envelope); err != nil {
return err
}
r, err := st.HashTreeRoot(ctx)
if err != nil {
return errors.Wrap(err, "could not get hash tree root")
}
if r != envelope.StateRoot() {
return fmt.Errorf("state root mismatch: expected %#x, got %#x", envelope.StateRoot(), r)
}
return nil
}
// ApplyExecutionPayload applies the execution payload envelope to the state and performs the same
// consistency checks as the full processing path. This keeps the post-payload state root computation
// on a shared code path, even though some bid/payload checks are not strictly required for the root itself.
func ApplyExecutionPayload(
ctx context.Context,
st state.BeaconState,
envelope interfaces.ROExecutionPayloadEnvelope,
) error {
latestHeader := st.LatestBlockHeader()
if len(latestHeader.StateRoot) == 0 || bytes.Equal(latestHeader.StateRoot, make([]byte, 32)) {
previousStateRoot, err := st.HashTreeRoot(ctx)
@@ -128,10 +156,6 @@ func ProcessExecutionPayload(
if err != nil {
return errors.Wrap(err, "could not compute block header root")
}
envelope, err := signedEnvelope.Envelope()
if err != nil {
return errors.Wrap(err, "could not get envelope from signed envelope")
}
beaconBlockRoot := envelope.BeaconBlockRoot()
if !bytes.Equal(beaconBlockRoot[:], blockHeaderRoot[:]) {
@@ -217,14 +241,6 @@ func ProcessExecutionPayload(
return errors.Wrap(err, "could not set latest block hash")
}
r, err := st.HashTreeRoot(ctx)
if err != nil {
return errors.Wrap(err, "could not get hash tree root")
}
if r != envelope.StateRoot() {
return fmt.Errorf("state root mismatch: expected %#x, got %#x", envelope.StateRoot(), r)
}
return nil
}

View File

@@ -268,10 +268,23 @@ func (s *Service) validateCommitteeIndexAndCount(
a eth.Att,
bs state.ReadOnlyBeaconState,
) (primitives.CommitteeIndex, uint64, pubsub.ValidationResult, error) {
// - [REJECT] attestation.data.index == 0
if a.Version() >= version.Electra && a.GetData().CommitteeIndex != 0 {
return 0, 0, pubsub.ValidationReject, errors.New("attestation data's committee index must be 0")
// Validate committee index based on fork.
if a.Version() >= version.Electra {
data := a.GetData()
attEpoch := slots.ToEpoch(data.Slot)
isGloas := attEpoch >= params.BeaconConfig().GloasForkEpoch
if isGloas {
if result, err := s.validateGloasCommitteeIndex(data); result != pubsub.ValidationAccept {
return 0, 0, result, err
}
} else {
// [REJECT] attestation.data.index == 0 (New in Electra, removed in Gloas)
if data.CommitteeIndex != 0 {
return 0, 0, pubsub.ValidationReject, errors.New("attestation data's committee index must be 0")
}
}
}
valCount, err := helpers.ActiveValidatorCount(ctx, bs, slots.ToEpoch(a.GetData().Slot))
if err != nil {
return 0, 0, pubsub.ValidationIgnore, err
@@ -356,6 +369,29 @@ func validateAttestingIndex(
return pubsub.ValidationAccept, nil
}
// validateGloasCommitteeIndex validates committee index rules for Gloas fork.
// [REJECT] attestation.data.index < 2. (New in Gloas)
// [REJECT] attestation.data.index == 0 if block.slot == attestation.data.slot. (New in Gloas)
func (s *Service) validateGloasCommitteeIndex(data *eth.AttestationData) (pubsub.ValidationResult, error) {
if data.CommitteeIndex >= 2 {
return pubsub.ValidationReject, errors.New("attestation data's committee index must be < 2")
}
// Same-slot attestations must use committee index 0
if data.CommitteeIndex != 0 {
blockRoot := bytesutil.ToBytes32(data.BeaconBlockRoot)
slot, err := s.cfg.chain.RecentBlockSlot(blockRoot)
if err != nil {
return pubsub.ValidationIgnore, err
}
if slot == data.Slot {
return pubsub.ValidationReject, errors.New("same slot attestations must use committee index 0")
}
}
return pubsub.ValidationAccept, nil
}
// generateUnaggregatedAttCacheKey generates the cache key for unaggregated attestation tracking.
func generateUnaggregatedAttCacheKey(att eth.Att) (string, error) {
var attester uint64

View File

@@ -684,3 +684,75 @@ func Test_validateCommitteeIndexAndCount_Boundary(t *testing.T) {
require.ErrorContains(t, "committee index", err)
require.Equal(t, pubsub.ValidationReject, res)
}
func Test_validateGloasCommitteeIndex(t *testing.T) {
tests := []struct {
name string
committeeIndex primitives.CommitteeIndex
attestationSlot primitives.Slot
blockSlot primitives.Slot
wantResult pubsub.ValidationResult
wantErr string
}{
{
name: "committee index >= 2 should reject",
committeeIndex: 2,
attestationSlot: 10,
blockSlot: 10,
wantResult: pubsub.ValidationReject,
wantErr: "committee index must be < 2",
},
{
name: "committee index 0 should accept",
committeeIndex: 0,
attestationSlot: 10,
blockSlot: 10,
wantResult: pubsub.ValidationAccept,
wantErr: "",
},
{
name: "committee index 1 different-slot should accept",
committeeIndex: 1,
attestationSlot: 10,
blockSlot: 9,
wantResult: pubsub.ValidationAccept,
wantErr: "",
},
{
name: "committee index 1 same-slot should reject",
committeeIndex: 1,
attestationSlot: 10,
blockSlot: 10,
wantResult: pubsub.ValidationReject,
wantErr: "same slot attestations must use committee index 0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockChain := &mockChain.ChainService{
BlockSlot: tt.blockSlot,
}
s := &Service{
cfg: &config{
chain: mockChain,
},
}
data := &ethpb.AttestationData{
Slot: tt.attestationSlot,
CommitteeIndex: tt.committeeIndex,
BeaconBlockRoot: bytesutil.PadTo([]byte("blockroot"), 32),
}
result, err := s.validateGloasCommitteeIndex(data)
require.Equal(t, tt.wantResult, result)
if tt.wantErr != "" {
require.ErrorContains(t, tt.wantErr, err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,3 @@
### Added
- Add gossip beacon attestation validation conditions for Gloas fork

View File

@@ -0,0 +1,2 @@
### Ignored
- Refactor ProcessExecutionPayload to ApplyExecutionPayload