From 4eabe32e268ca411823c3554debfdcb9632fa86a Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 13 May 2026 22:34:40 +0800 Subject: [PATCH] core, params: implement bal size constraint --- core/block_validator.go | 8 +++- core/types/bal/bal_encoding.go | 26 ++++++++++++ core/types/bal/bal_test.go | 73 ++++++++++++++++++++++++++++++++++ params/protocol_params.go | 10 +++++ 4 files changed, 116 insertions(+), 1 deletion(-) diff --git a/core/block_validator.go b/core/block_validator.go index f5c5f11d14..e91eadabff 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -126,6 +126,8 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { return fmt.Errorf("access list hash mismatch, computed: %x, remote: %x", computed, *block.Header().BlockAccessListHash) } else if err := block.AccessList().Validate(); err != nil { return fmt.Errorf("invalid block access list: %v", err) + } else if err := block.AccessList().ValidateSize(block.GasLimit()); err != nil { + return fmt.Errorf("invalid block access list: %v", err) } } } else if block.Header().BlockAccessListHash != nil || block.AccessList() != nil { @@ -183,10 +185,14 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD } // Verify Block-level accessList once Amsterdam is enabled if v.config.IsAmsterdam(block.Number(), block.Time()) { - local, remote := res.Bal.ToEncodingObj().Hash(), *block.Header().BlockAccessListHash + enc := res.Bal.ToEncodingObj() + local, remote := enc.Hash(), *block.Header().BlockAccessListHash if local != remote { return fmt.Errorf("access list hash mismatch, local: %x, remote: %x", local, remote) } + if err := enc.ValidateSize(block.GasLimit()); err != nil { + return fmt.Errorf("invalid block access list: %v", err) + } } // Validate the state root against the received state root and throw // an error if they don't match. diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index ff36612eb0..5d7c2856f1 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -92,6 +92,32 @@ func (e *BlockAccessList) Validate() error { return nil } +// ItemCount returns the number of items in the BAL for EIP-7928 size-constraint +// purposes: the count of distinct addresses plus every storage key (writes + +// reads) carried by those accounts. A storage slot is counted once regardless +// of how many transactions wrote to it. +func (e *BlockAccessList) ItemCount() uint64 { + count := uint64(len(*e)) // distinct addresses + for i := range *e { + count += uint64(len((*e)[i].StorageWrites)) + uint64(len((*e)[i].StorageReads)) + } + return count +} + +// ValidateSize returns an error if the BAL violates the EIP-7928 size +// constraint for the given block gas limit: +// +// ItemCount() <= blockGasLimit / params.BALItemCost +func (e *BlockAccessList) ValidateSize(blockGasLimit uint64) error { + items := e.ItemCount() + limit := blockGasLimit / params.BALItemCost + if items > limit { + return fmt.Errorf("block access list exceeds size constraint: items=%d, limit=%d (block gas limit %d / %d)", + items, limit, blockGasLimit, params.BALItemCost) + } + return nil +} + // Hash computes the keccak256 hash of the access list func (e *BlockAccessList) Hash() common.Hash { var enc bytes.Buffer diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 4bd705034b..ca97b92cb5 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/internal/testrand" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/holiman/uint256" ) @@ -281,6 +282,78 @@ func TestBlockAccessListCopy(t *testing.T) { } } +func TestBlockAccessListItemCount(t *testing.T) { + empty := &BlockAccessList{} + if got := empty.ItemCount(); got != 0 { + t.Fatalf("empty BAL item count: got %d, want 0", got) + } + + addr1 := [20]byte(testrand.Bytes(20)) + addr2 := [20]byte(testrand.Bytes(20)) + one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } + bal := &BlockAccessList{ + AccountAccess{ + Address: addr1, + StorageWrites: []encodingSlotWrites{ + {Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}, {TxIdx: 1, ValueAfter: one()}}}, + {Slot: one()}, + }, + StorageReads: []*uint256.Int{one()}, + }, + AccountAccess{Address: addr2}, // address-only, no slots + } + // 2 addresses + 2 write-slots + 1 read-slot = 5 items. + // (Multiple TxIdx writes to the same slot count as ONE item.) + if got := bal.ItemCount(); got != 5 { + t.Fatalf("item count: got %d, want 5", got) + } +} + +func TestBlockAccessListValidateSize(t *testing.T) { + // Build a BAL with exactly 30 items: 3 addresses, each with 9 storage + // slots (some writes, some reads). 3 + 9*3 = 30. + one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } + bal := make(BlockAccessList, 3) + for i := range bal { + bal[i].Address = [20]byte(testrand.Bytes(20)) + for j := 0; j < 5; j++ { + bal[i].StorageWrites = append(bal[i].StorageWrites, encodingSlotWrites{ + Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}}, + }) + } + for j := 0; j < 4; j++ { + bal[i].StorageReads = append(bal[i].StorageReads, one()) + } + } + if got := bal.ItemCount(); got != 30 { + t.Fatalf("setup: item count = %d, want 30", got) + } + + // limit = blockGasLimit / BALItemCost. + // 30 items requires limit >= 30, i.e. gasLimit >= 30 * 2000 = 60_000. + tests := []struct { + name string + gasLimit uint64 + expectError bool + }{ + {"exactly at limit", 30 * params.BALItemCost, false}, + {"well above limit", 60_000_000, false}, + {"one below limit", 30*params.BALItemCost - 1, true}, + {"zero gas limit", 0, true}, + } + for _, tc := range tests { + err := bal.ValidateSize(tc.gasLimit) + if (err != nil) != tc.expectError { + t.Errorf("%s: got err=%v, expectError=%v", tc.name, err, tc.expectError) + } + } + + // Empty BAL is always valid (even with 0 gas limit). + if err := (&BlockAccessList{}).ValidateSize(0); err != nil { + t.Fatalf("empty BAL must pass any limit: %v", err) + } +} + func TestBlockAccessListValidation(t *testing.T) { // Validate the block access list after RLP decoding enc := makeTestBAL(true) diff --git a/params/protocol_params.go b/params/protocol_params.go index 9da275c486..3e36b83547 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -186,6 +186,16 @@ const ( HistoryServeWindow = 8191 // Number of blocks to serve historical block hashes for, EIP-2935. MaxBlockSize = 8_388_608 // maximum size of an RLP-encoded block + + // BALItemCost is the gas-cost divisor for the EIP-7928 block access list + // size constraint: bal_items <= block_gas_limit / BALItemCost, where + // bal_items counts every distinct address in the BAL plus every storage + // key (writes + reads) carried by those accounts. + // + // The value (2000) is set deliberately below COLD_SLOAD_COST (2100) so + // the bound has a small safety margin for system-contract accesses that + // don't consume block gas. + BALItemCost uint64 = 2000 ) // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation