Skip to content

EIP-7928: Add BAL max items cap and slot uniqueness validation#19627

Merged
taratorio merged 3 commits into
mainfrom
fix/eip-7928-max-items
Mar 9, 2026
Merged

EIP-7928: Add BAL max items cap and slot uniqueness validation#19627
taratorio merged 3 commits into
mainfrom
fix/eip-7928-max-items

Conversation

@mh0lt

@mh0lt mh0lt commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements two new constraints from the EIP-7928 bal-devnet-3 spec update:

  1. Max items cap: bal_items <= block_gas_limit / 2000, where bal_items counts each
    address 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.

  2. Slot uniqueness: A storage key must not appear in both storage_changes and
    storage_reads for the same account. A slot is either written or read-only, never both.

Changes

execution/types/block_access_list.go

  • Added BalItemCost = 2000 constant
  • Added ValidateMaxItems(blockGasLimit uint64) error method that counts addresses +
    storage keys and rejects BALs exceeding the gas-derived limit
  • Added slot uniqueness check in AccountChanges.validate() — builds a set of changed
    slots and rejects any that also appear in reads

execution/stagedsync/bal_create.go

  • Called ValidateMaxItems(header.GasLimit) in ProcessBAL alongside the existing
    Validate() call

execution/types/block_access_list_test.go

  • TestBlockAccessListValidateMaxItems — tests exactly at limit, over limit, and empty BAL
  • TestBlockAccessListSlotUniqueness — tests that a slot in both changes and reads is rejected

execution/tests/block_test.go

  • Consolidated 46 individual EIP-7928 skipLoad lines into a single pattern skip
  • All EIP-7928 tests are still skipped pending EIP-8037 state creation gas cost (PR#19596)

Testing 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

@mh0lt mh0lt requested a review from yperbasis as a code owner March 4, 2026 18:06
@mh0lt mh0lt requested review from shohamc1 and taratorio March 4, 2026 18:20
@mh0lt mh0lt added the Glamsterdam https://eips.ethereum.org/EIPS/eip-7773 label Mar 4, 2026
@mh0lt mh0lt added this to the 3.5.0 milestone Mar 4, 2026
@yperbasis yperbasis requested a review from Copilot March 5, 2026 13:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 / 2000 and enforce it during BAL processing.
  • Enforce per-account slot uniqueness: a storage key cannot appear in both storage_changes and storage_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.

Comment on lines +895 to +903
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())
}

Copilot AI Mar 5, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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++
}

Copilot uses AI. Check for mistakes.
Comment on lines +873 to +880
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)
}

Copilot AI Mar 5, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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)
}
}

Copilot uses AI. Check for mistakes.
@taratorio taratorio merged commit d38aee1 into main Mar 9, 2026
36 checks passed
@taratorio taratorio deleted the fix/eip-7928-max-items branch March 9, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Glamsterdam https://eips.ethereum.org/EIPS/eip-7773

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants