mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-02-14 06:55:07 -05:00
Compare commits
1 Commits
calculate-
...
gossip-att
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d524bf6ef |
@@ -116,32 +116,17 @@ func CalculateStateRoot(
|
||||
rollback state.BeaconState,
|
||||
signed interfaces.ReadOnlySignedBeaconBlock,
|
||||
) ([32]byte, error) {
|
||||
st, err := CalculatePostState(ctx, rollback, signed)
|
||||
if err != nil {
|
||||
return [32]byte{}, err
|
||||
}
|
||||
return st.HashTreeRoot(ctx)
|
||||
}
|
||||
|
||||
// CalculatePostState returns the post-block state after processing the given
|
||||
// block on a copy of the input state. It is identical to CalculateStateRoot
|
||||
// but returns the full state instead of just its hash tree root.
|
||||
func CalculatePostState(
|
||||
ctx context.Context,
|
||||
rollback state.BeaconState,
|
||||
signed interfaces.ReadOnlySignedBeaconBlock,
|
||||
) (state.BeaconState, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "core.state.CalculatePostState")
|
||||
ctx, span := trace.StartSpan(ctx, "core.state.CalculateStateRoot")
|
||||
defer span.End()
|
||||
if ctx.Err() != nil {
|
||||
tracing.AnnotateError(span, ctx.Err())
|
||||
return nil, ctx.Err()
|
||||
return [32]byte{}, ctx.Err()
|
||||
}
|
||||
if rollback == nil || rollback.IsNil() {
|
||||
return nil, errors.New("nil state")
|
||||
return [32]byte{}, errors.New("nil state")
|
||||
}
|
||||
if signed == nil || signed.IsNil() || signed.Block().IsNil() {
|
||||
return nil, errors.New("nil block")
|
||||
return [32]byte{}, errors.New("nil block")
|
||||
}
|
||||
|
||||
// Copy state to avoid mutating the state reference.
|
||||
@@ -152,22 +137,22 @@ func CalculatePostState(
|
||||
parentRoot := signed.Block().ParentRoot()
|
||||
state, err = ProcessSlotsUsingNextSlotCache(ctx, state, parentRoot[:], signed.Block().Slot())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not process slots")
|
||||
return [32]byte{}, errors.Wrap(err, "could not process slots")
|
||||
}
|
||||
|
||||
// Execute per block transition.
|
||||
if features.Get().EnableProposerPreprocessing {
|
||||
state, err = processBlockForProposing(ctx, state, signed)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not process block for proposing")
|
||||
return [32]byte{}, errors.Wrap(err, "could not process block for proposing")
|
||||
}
|
||||
} else {
|
||||
state, err = ProcessBlockForStateRoot(ctx, state, signed)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not process block")
|
||||
return [32]byte{}, errors.Wrap(err, "could not process block")
|
||||
}
|
||||
}
|
||||
return state, nil
|
||||
return state.HashTreeRoot(ctx)
|
||||
}
|
||||
|
||||
// processBlockVerifySigs processes the block and verifies the signatures within it. Block signatures are not verified as this block is not yet signed.
|
||||
|
||||
@@ -609,31 +609,21 @@ func (vs *Server) GetFeeRecipientByPubKey(ctx context.Context, request *ethpb.Fe
|
||||
// computeStateRoot computes the state root after a block has been processed through a state transition and
|
||||
// returns it to the validator client.
|
||||
func (vs *Server) computeStateRoot(ctx context.Context, block interfaces.SignedBeaconBlock) ([]byte, error) {
|
||||
st, err := vs.computePostBlockState(ctx, block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, err := st.HashTreeRoot(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not compute state root")
|
||||
}
|
||||
log.WithField("beaconStateRoot", fmt.Sprintf("%#x", root)).Debugf("Computed state root")
|
||||
return root[:], nil
|
||||
}
|
||||
|
||||
// computePostBlockState computes the post-block state by running the state transition.
|
||||
// It uses the same logic as CalculateStateRoot (Copy, feature flags, slot processing)
|
||||
// but returns the full state instead of just its hash.
|
||||
func (vs *Server) computePostBlockState(ctx context.Context, block interfaces.SignedBeaconBlock) (state.BeaconState, error) {
|
||||
beaconState, err := vs.StateGen.StateByRoot(ctx, block.Block().ParentRoot())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve beacon state")
|
||||
}
|
||||
st, err := transition.CalculatePostState(ctx, beaconState, block)
|
||||
root, err := transition.CalculateStateRoot(
|
||||
ctx,
|
||||
beaconState,
|
||||
block,
|
||||
)
|
||||
if err != nil {
|
||||
return vs.handlePostBlockStateError(ctx, block, err)
|
||||
return vs.handleStateRootError(ctx, block, err)
|
||||
}
|
||||
return st, nil
|
||||
|
||||
log.WithField("beaconStateRoot", fmt.Sprintf("%#x", root)).Debugf("Computed state root")
|
||||
return root[:], nil
|
||||
}
|
||||
|
||||
type computeStateRootAttemptsKeyType string
|
||||
@@ -641,8 +631,8 @@ type computeStateRootAttemptsKeyType string
|
||||
const computeStateRootAttemptsKey = computeStateRootAttemptsKeyType("compute-state-root-attempts")
|
||||
const maxComputeStateRootAttempts = 3
|
||||
|
||||
// handlePostBlockStateError retries block construction in some error cases.
|
||||
func (vs *Server) handlePostBlockStateError(ctx context.Context, block interfaces.SignedBeaconBlock, err error) (state.BeaconState, error) {
|
||||
// handleStateRootError retries block construction in some error cases.
|
||||
func (vs *Server) handleStateRootError(ctx context.Context, block interfaces.SignedBeaconBlock, err error) ([]byte, error) {
|
||||
if ctx.Err() != nil {
|
||||
return nil, status.Errorf(codes.Canceled, "context error: %v", ctx.Err())
|
||||
}
|
||||
@@ -691,8 +681,8 @@ func (vs *Server) handlePostBlockStateError(ctx context.Context, block interface
|
||||
} else {
|
||||
ctx = context.WithValue(ctx, computeStateRootAttemptsKey, v+1)
|
||||
}
|
||||
// recursive call to compute post-block state again
|
||||
return vs.computePostBlockState(ctx, block)
|
||||
// recursive call to compute state root again
|
||||
return vs.computeStateRoot(ctx, block)
|
||||
}
|
||||
|
||||
// Deprecated: The gRPC API will remain the default and fully supported through v8 (expected in 2026) but will be eventually removed in favor of REST API.
|
||||
|
||||
@@ -1313,8 +1313,8 @@ func TestProposer_ComputeStateRoot_OK(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHandlePostBlockStateError_MaxAttemptsReached(t *testing.T) {
|
||||
// Test that handlePostBlockStateError returns an error when max attempts is reached
|
||||
func TestHandleStateRootError_MaxAttemptsReached(t *testing.T) {
|
||||
// Test that handleStateRootError returns an error when max attempts is reached
|
||||
// instead of recursing infinitely.
|
||||
ctx := t.Context()
|
||||
vs := &Server{}
|
||||
@@ -1327,15 +1327,15 @@ func TestHandlePostBlockStateError_MaxAttemptsReached(t *testing.T) {
|
||||
// Pre-seed the context with max attempts already reached
|
||||
ctx = context.WithValue(ctx, computeStateRootAttemptsKey, maxComputeStateRootAttempts)
|
||||
|
||||
// Call handlePostBlockStateError with a retryable error
|
||||
_, err = vs.handlePostBlockStateError(ctx, wsb, transition.ErrAttestationsSignatureInvalid)
|
||||
// Call handleStateRootError with a retryable error
|
||||
_, err = vs.handleStateRootError(ctx, wsb, transition.ErrAttestationsSignatureInvalid)
|
||||
|
||||
// Should return an error about max attempts instead of recursing
|
||||
require.ErrorContains(t, "attempted max compute state root attempts", err)
|
||||
}
|
||||
|
||||
func TestHandlePostBlockStateError_IncrementsAttempts(t *testing.T) {
|
||||
// Test that handlePostBlockStateError properly increments the attempts counter
|
||||
func TestHandleStateRootError_IncrementsAttempts(t *testing.T) {
|
||||
// Test that handleStateRootError properly increments the attempts counter
|
||||
// and eventually fails after max attempts.
|
||||
db := dbutil.SetupDB(t)
|
||||
ctx := t.Context()
|
||||
@@ -1357,10 +1357,10 @@ func TestHandlePostBlockStateError_IncrementsAttempts(t *testing.T) {
|
||||
// Add a state for the parent root so StateByRoot succeeds
|
||||
require.NoError(t, stateGen.SaveState(ctx, parentRoot, beaconState))
|
||||
|
||||
// Call handlePostBlockStateError with a retryable error - it will recurse
|
||||
// but eventually hit the max attempts limit since CalculatePostState
|
||||
// Call handleStateRootError with a retryable error - it will recurse
|
||||
// but eventually hit the max attempts limit since CalculateStateRoot
|
||||
// will keep failing (no valid attestations, randao, etc.)
|
||||
_, err = vs.handlePostBlockStateError(ctx, wsb, transition.ErrAttestationsSignatureInvalid)
|
||||
_, err = vs.handleStateRootError(ctx, wsb, transition.ErrAttestationsSignatureInvalid)
|
||||
|
||||
// Should eventually fail - either with max attempts or another error
|
||||
require.NotNil(t, err)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 := ðpb.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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
### Ignored
|
||||
|
||||
- refactors calculate state root and breaks up into calculate post state function.
|
||||
3
changelog/terence_gloas-attestation-validation.md
Normal file
3
changelog/terence_gloas-attestation-validation.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- Add gossip beacon attestation validation conditions for Gloas fork
|
||||
Reference in New Issue
Block a user