EIP-7928: Add BAL max items cap and slot uniqueness validation#19627
Conversation
There was a problem hiding this comment.
Pull request overview
Adds EIP-7928 bal-devnet-3 validation constraints to keep Block Access Lists (BALs) bounded and internally consistent during Amsterdam/experimental processing.
Changes:
- Add a BAL max-items cap derived from
block_gas_limit / 2000and enforce it during BAL processing. - Enforce per-account slot uniqueness: a storage key cannot appear in both
storage_changesandstorage_reads. - Add unit tests for the new validation rules and simplify EIP-7928 devnet test skipping via a directory pattern.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
execution/types/block_access_list.go |
Introduces BalItemCost, adds ValidateMaxItems, and enforces slot uniqueness in AccountChanges.validate() |
execution/stagedsync/bal_create.go |
Calls ValidateMaxItems(header.GasLimit) during ProcessBAL |
execution/types/block_access_list_test.go |
Adds tests for max-items cap and slot uniqueness validation |
execution/tests/block_test.go |
Replaces many individual EIP-7928 skip patterns with a single directory-pattern skip |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if len(ac.StorageChanges) > 0 && len(ac.StorageReads) > 0 { | ||
| changeSlots := make(map[common.Hash]struct{}, len(ac.StorageChanges)) | ||
| for _, sc := range ac.StorageChanges { | ||
| changeSlots[sc.Slot.Value()] = struct{}{} | ||
| } | ||
| for _, key := range ac.StorageReads { | ||
| if _, exists := changeSlots[key.Value()]; exists { | ||
| return fmt.Errorf("storage key %s in both changes and reads", key.Value().Hex()) | ||
| } |
There was a problem hiding this comment.
The slot-uniqueness check builds a map of all changed slots. Since validateSlotChangeList() and validateStorageReads() already enforce strictly increasing order, this overlap check can be implemented with a two-pointer merge (or binary search) to avoid per-account map allocations and reduce memory/CPU overhead for large BALs.
| if len(ac.StorageChanges) > 0 && len(ac.StorageReads) > 0 { | |
| changeSlots := make(map[common.Hash]struct{}, len(ac.StorageChanges)) | |
| for _, sc := range ac.StorageChanges { | |
| changeSlots[sc.Slot.Value()] = struct{}{} | |
| } | |
| for _, key := range ac.StorageReads { | |
| if _, exists := changeSlots[key.Value()]; exists { | |
| return fmt.Errorf("storage key %s in both changes and reads", key.Value().Hex()) | |
| } | |
| // Since validateSlotChangeList() and validateStorageReads() enforce strictly increasing | |
| // order of slots, we can detect overlaps with a two-pointer merge instead of building | |
| // a map of all changed slots. | |
| if len(ac.StorageChanges) > 0 && len(ac.StorageReads) > 0 { | |
| i, j := 0, 0 | |
| for i < len(ac.StorageChanges) && j < len(ac.StorageReads) { | |
| changeSlot := ac.StorageChanges[i].Slot.Value() | |
| readSlot := ac.StorageReads[j].Value() | |
| if changeSlot == readSlot { | |
| return fmt.Errorf("storage key %s in both changes and reads", readSlot.Hex()) | |
| } | |
| if bytes.Compare(changeSlot[:], readSlot[:]) < 0 { | |
| i++ | |
| } else { | |
| j++ | |
| } |
| for _, ac := range bal { | ||
| items++ // each address counts as 1 item | ||
| items += uint64(len(ac.StorageChanges)) | ||
| items += uint64(len(ac.StorageReads)) | ||
| } | ||
| if items > maxItems { | ||
| return fmt.Errorf("block access list too large: %d items > %d max (gas limit %d / %d)", items, maxItems, blockGasLimit, BalItemCost) | ||
| } |
There was a problem hiding this comment.
ValidateMaxItems can panic if the BAL contains a nil *AccountChanges entry (it dereferences ac without checking). Consider mirroring Validate() by returning an error on nil entries (or explicitly documenting/enforcing that Validate() must be called first) and optionally short-circuiting once items exceeds maxItems to avoid unnecessary work/overflow risk.
| for _, ac := range bal { | |
| items++ // each address counts as 1 item | |
| items += uint64(len(ac.StorageChanges)) | |
| items += uint64(len(ac.StorageReads)) | |
| } | |
| if items > maxItems { | |
| return fmt.Errorf("block access list too large: %d items > %d max (gas limit %d / %d)", items, maxItems, blockGasLimit, BalItemCost) | |
| } | |
| for i, ac := range bal { | |
| if ac == nil { | |
| return fmt.Errorf("entry %d is nil", i) | |
| } | |
| // Each address counts as 1 item. | |
| items++ | |
| if items > maxItems { | |
| return fmt.Errorf("block access list too large: %d items > %d max (gas limit %d / %d)", items, maxItems, blockGasLimit, BalItemCost) | |
| } | |
| // Count storage-related items, guarding against uint64 overflow. | |
| storageItems := uint64(len(ac.StorageChanges) + len(ac.StorageReads)) | |
| if storageItems > math.MaxUint64-items { | |
| // Clamp to a value greater than maxItems to ensure the constraint is treated as violated. | |
| items = maxItems + 1 | |
| } else { | |
| items += storageItems | |
| } | |
| if items > maxItems { | |
| return fmt.Errorf("block access list too large: %d items > %d max (gas limit %d / %d)", items, maxItems, blockGasLimit, BalItemCost) | |
| } | |
| } |
Summary
Implements two new constraints from the EIP-7928 bal-devnet-3 spec update:
Max items cap:
bal_items <= block_gas_limit / 2000, wherebal_itemscounts eachaddress and each storage key (both changes and reads). This is an anti-DOS measure that
bounds the BAL size relative to the block gas limit.
Slot uniqueness: A storage key must not appear in both
storage_changesandstorage_readsfor the same account. A slot is either written or read-only, never both.Changes
execution/types/block_access_list.goBalItemCost = 2000constantValidateMaxItems(blockGasLimit uint64) errormethod that counts addresses +storage keys and rejects BALs exceeding the gas-derived limit
AccountChanges.validate()— builds a set of changedslots and rejects any that also appear in reads
execution/stagedsync/bal_create.goValidateMaxItems(header.GasLimit)inProcessBALalongside the existingValidate()callexecution/types/block_access_list_test.goTestBlockAccessListValidateMaxItems— tests exactly at limit, over limit, and empty BALTestBlockAccessListSlotUniqueness— tests that a slot in both changes and reads is rejectedexecution/tests/block_test.goskipLoadlines into a single pattern skipTesting Note
EIP-7928 bal-devnet-3 test fixtures also require EIP-8037 (state creation gas cost increase, #19596) to pass — the gas used mismatches are from EIP-8037, not from the decode formulas. The decode changes are verified correct by matching go-ethereum's implementation. Hive tests for EIP-8024 are similarly blocked on EIP-8037