From 3703c08b80378c73f7495d089112ab69cea9f918 Mon Sep 17 00:00:00 2001 From: Vansh Sahay Date: Sat, 8 Aug 2026 06:02:56 +0530 Subject: [PATCH 1/2] ethapi: add eth_getBlockAccessList --- internal/ethapi/block_access_list.go | 140 ++++++++++++++++++ internal/ethapi/block_access_list_test.go | 164 ++++++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 internal/ethapi/block_access_list.go create mode 100644 internal/ethapi/block_access_list_test.go diff --git a/internal/ethapi/block_access_list.go b/internal/ethapi/block_access_list.go new file mode 100644 index 00000000000..2dacb925bd9 --- /dev/null +++ b/internal/ethapi/block_access_list.go @@ -0,0 +1,140 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ethapi + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" +) + +// The result types below mirror the execution-apis block access list schema: +// each change entry pairs a 32-bit block-access index with the post-state +// value, storage slots pair a 32-byte key with per-index writes, balance +// values are 256-bit quantities and storage/read values are 32-byte ones. + +type storageChangeResult struct { + Index hexutil.Uint64 `json:"index"` + Value common.Hash `json:"value"` +} + +type slotChangesResult struct { + Key common.Hash `json:"key"` + Changes []storageChangeResult `json:"changes"` +} + +type balanceChangeResult struct { + Index hexutil.Uint64 `json:"index"` + Value *hexutil.Big `json:"value"` +} + +type nonceChangeResult struct { + Index hexutil.Uint64 `json:"index"` + Value hexutil.Uint64 `json:"value"` +} + +type codeChangeResult struct { + Index hexutil.Uint64 `json:"index"` + Code hexutil.Bytes `json:"code"` +} + +type accountAccessResult struct { + Address common.Address `json:"address"` + BalanceChanges []balanceChangeResult `json:"balanceChanges"` + CodeChanges []codeChangeResult `json:"codeChanges"` + NonceChanges []nonceChangeResult `json:"nonceChanges"` + StorageChanges []slotChangesResult `json:"storageChanges"` + StorageReads []common.Hash `json:"storageReads"` +} + +// GetBlockAccessList returns the block access list for the given block. +// +// The list is a lexicographically sorted array of account accesses, each +// describing the per-transaction post-state changes (balance, nonce, code, +// storage writes) and the storage reads performed on the account during +// block execution. +// +// Blocks predating the fork carrying the block access list (or otherwise +// lacking one) return an empty list. A null result means the block does not +// exist. +func (api *BlockChainAPI) GetBlockAccessList(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]accountAccessResult, error) { + block, err := api.b.BlockByNumberOrHash(ctx, blockNrOrHash) + if block == nil || err != nil { + return nil, err + } + al := block.AccessList() + if al == nil { + return []accountAccessResult{}, nil + } + out := make([]accountAccessResult, 0, len(*al)) + for _, acc := range *al { + // The spec requires all change arrays to be present (possibly empty). + res := accountAccessResult{ + Address: acc.Address, + BalanceChanges: make([]balanceChangeResult, 0, len(acc.BalanceChanges)), + NonceChanges: make([]nonceChangeResult, 0, len(acc.NonceChanges)), + CodeChanges: make([]codeChangeResult, 0, len(acc.CodeChanges)), + StorageChanges: make([]slotChangesResult, 0, len(acc.StorageChanges)), + StorageReads: make([]common.Hash, 0, len(acc.StorageReads)), + } + + for _, change := range acc.BalanceChanges { + res.BalanceChanges = append(res.BalanceChanges, balanceChangeResult{ + Index: hexutil.Uint64(change.BlockAccessIndex), + Value: (*hexutil.Big)(change.PostBalance.ToBig()), + }) + } + for _, change := range acc.NonceChanges { + res.NonceChanges = append(res.NonceChanges, nonceChangeResult{ + Index: hexutil.Uint64(change.BlockAccessIndex), + Value: hexutil.Uint64(change.PostNonce), + }) + } + for _, change := range acc.CodeChanges { + res.CodeChanges = append(res.CodeChanges, codeChangeResult{ + Index: hexutil.Uint64(change.BlockAccessIndex), + Code: hexutil.Bytes(change.NewCode), + }) + } + for _, slot := range acc.StorageChanges { + changes := make([]storageChangeResult, 0, len(slot.SlotChanges)) + for _, write := range slot.SlotChanges { + var value [32]byte + write.PostValue.WriteToSlice(value[:]) + changes = append(changes, storageChangeResult{ + Index: hexutil.Uint64(write.BlockAccessIndex), + Value: common.Hash(value), + }) + } + var key [32]byte + slot.Slot.WriteToSlice(key[:]) + res.StorageChanges = append(res.StorageChanges, slotChangesResult{ + Key: common.Hash(key), + Changes: changes, + }) + } + for _, read := range acc.StorageReads { + var key [32]byte + read.WriteToSlice(key[:]) + res.StorageReads = append(res.StorageReads, common.Hash(key)) + } + out = append(out, res) + } + return out, nil +} diff --git a/internal/ethapi/block_access_list_test.go b/internal/ethapi/block_access_list_test.go new file mode 100644 index 00000000000..443ac024ca5 --- /dev/null +++ b/internal/ethapi/block_access_list_test.go @@ -0,0 +1,164 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ethapi + +import ( + "bytes" + "context" + "encoding/json" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" +) + +// TestGetBlockAccessList exercises eth_getBlockAccessList against a chain +// with the Amsterdam fork active, asserting that the stored block access list +// is returned for number, tag and hash lookups, and that the JSON encoding +// follows the execution-apis spec field names. +func TestGetBlockAccessList(t *testing.T) { + t.Parallel() + accounts := newAccounts(2) + + cfg := *params.MergedTestChainConfig + cfg.AmsterdamTime = uint64Ptr(0) // activate the block access list fork + + genBlocks := 3 + api := NewBlockChainAPI(newTestBackend(t, genBlocks, &core.Genesis{ + Config: &cfg, + Alloc: types.GenesisAlloc{accounts[0].addr: {Balance: big.NewInt(params.Ether)}}, + }, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{ + Nonce: uint64(i), + To: &accounts[1].addr, + Value: big.NewInt(1000), + Gas: params.TxGas, + GasPrice: b.BaseFee(), + }), types.HomesteadSigner{}, accounts[0].key) + b.AddTx(tx) + b.SetPoS() + })) + + latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + + // Resolve the latest block once for the by-hash lookup below. + block, err := api.b.BlockByNumberOrHash(context.Background(), latest) + if err != nil { + t.Fatalf("failed to resolve latest block: %v", err) + } + + var testSuite = []struct { + name string + ref rpc.BlockNumberOrHash + }{ + {"by-number", latest}, + {"by-hash", rpc.BlockNumberOrHashWithHash(block.Hash(), false)}, + } + for _, tc := range testSuite { + t.Run(tc.name, func(t *testing.T) { + al, err := api.GetBlockAccessList(context.Background(), tc.ref) + if err != nil { + t.Fatalf("GetBlockAccessList error: %v", err) + } + if al == nil { + t.Fatal("expected a non-nil access list") + } + if len(al) == 0 { + t.Fatal("expected a non-empty access list for a block with transactions") + } + // The list is sorted lexicographically by address; the coinbase + // (fee recipient) and both transfer accounts must be present. + var found [3]bool + want := []common.Address{{}, accounts[0].addr, accounts[1].addr} + for i, entry := range al { + if i > 0 && bytes.Compare(al[i-1].Address[:], entry.Address[:]) >= 0 { + t.Fatalf("access list not sorted at index %d: %s >= %s", i, al[i-1].Address, entry.Address) + } + for j, addr := range want { + if entry.Address == addr { + found[j] = true + } + } + } + for j, addr := range want { + if !found[j] { + t.Fatalf("access list missing expected account %s", addr) + } + } + }) + } + + // The JSON encoding must follow the execution-apis spec field names. + al, err := api.GetBlockAccessList(context.Background(), latest) + if err != nil { + t.Fatalf("GetBlockAccessList error: %v", err) + } + blob, err := json.Marshal(al) + if err != nil { + t.Fatalf("json.Marshal error: %v", err) + } + var decoded []map[string]any + if err := json.Unmarshal(blob, &decoded); err != nil { + t.Fatalf("json.Unmarshal error: %v", err) + } + for _, want := range []string{"address", "balanceChanges", "nonceChanges", "codeChanges", "storageChanges", "storageReads"} { + if _, ok := decoded[0][want]; !ok { + t.Fatalf("access list entry missing spec field %q: %s", want, blob) + } + } + for _, field := range []string{"balanceChanges", "nonceChanges", "codeChanges", "storageChanges"} { + if !hasSpecInnerFields(decoded[0][field]) { + t.Fatalf("spec field %q has wrong inner shape: %s", field, blob) + } + } + + // Unknown blocks must yield null, not an error. + if al, err := api.GetBlockAccessList(context.Background(), rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(genBlocks+100))); al != nil || err != nil { + t.Fatalf("expected null for unknown block, got list=%v err=%v", al, err) + } +} + +// hasSpecInnerFields checks that the change array uses the execution-apis +// index/value (or key/changes for storage) naming rather than the internal +// encoding naming. +func hasSpecInnerFields(v any) bool { + entries, ok := v.([]any) + if !ok || len(entries) == 0 { + return true // empty arrays are fine + } + entry, ok := entries[0].(map[string]any) + if !ok { + return false + } + if _, ok := entry["index"]; ok { + return true + } + if _, ok := entry["key"]; ok { + if _, ok := entry["changes"]; ok { + return true + } + } + return false +} + +func uint64Ptr(v uint64) *uint64 { return &v } From 931fd5980db003f97e9c08fef2750c7a30b6f3b4 Mon Sep 17 00:00:00 2001 From: Vansh Sahay Date: Sat, 8 Aug 2026 06:02:56 +0530 Subject: [PATCH 2/2] ethclient: add GetBlockAccessList --- ethclient/block_access_list.go | 81 ++++++++++++++++++++++++++++++++++ ethclient/ethclient_test.go | 74 +++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 ethclient/block_access_list.go diff --git a/ethclient/block_access_list.go b/ethclient/block_access_list.go new file mode 100644 index 00000000000..44ce9eb57e9 --- /dev/null +++ b/ethclient/block_access_list.go @@ -0,0 +1,81 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ethclient + +import ( + "context" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" +) + +// BlockAccessListEntry describes the state changes of a single account within +// a block access list, keyed by the transaction that caused them. +type BlockAccessListEntry struct { + Address common.Address `json:"address"` + BalanceChanges []BalanceChangeEntry `json:"balanceChanges"` + CodeChanges []CodeChangeEntry `json:"codeChanges"` + NonceChanges []NonceChangeEntry `json:"nonceChanges"` + StorageChanges []StorageChangesEntry `json:"storageChanges"` + StorageReads []common.Hash `json:"storageReads"` +} + +// BalanceChangeEntry records the post-state balance of an account at a block +// access index. +type BalanceChangeEntry struct { + Index hexutil.Uint64 `json:"index"` + Value *hexutil.Big `json:"value"` +} + +// CodeChangeEntry records the code deployed to an account at a block access +// index. +type CodeChangeEntry struct { + Index hexutil.Uint64 `json:"index"` + Code hexutil.Bytes `json:"code"` +} + +// NonceChangeEntry records the post-state nonce of an account at a block +// access index. +type NonceChangeEntry struct { + Index hexutil.Uint64 `json:"index"` + Value hexutil.Uint64 `json:"value"` +} + +// StorageChangeEntry records the post-state value of a storage slot at a +// block access index. +type StorageChangeEntry struct { + Index hexutil.Uint64 `json:"index"` + Value common.Hash `json:"value"` +} + +// StorageChangesEntry pairs a storage key with the writes performed on it. +type StorageChangesEntry struct { + Key common.Hash `json:"key"` + Changes []StorageChangeEntry `json:"changes"` +} + +// GetBlockAccessList returns the block access list of the given block. +func (ec *Client) GetBlockAccessList(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]BlockAccessListEntry, error) { + var r []BlockAccessListEntry + err := ec.c.CallContext(ctx, &r, "eth_getBlockAccessList", blockNrOrHash) + if err == nil && r == nil { + return nil, ethereum.NotFound + } + return r, err +} diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index 902a7da7516..d39207775ce 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" @@ -1058,3 +1059,76 @@ func genesisAlloc() types.GenesisAlloc { alloc[revertContractAddr] = types.Account{Code: revertCode} return alloc } + +type blockAccessListTestService struct { + calls chan rpc.BlockNumberOrHash +} + +func (s *blockAccessListTestService) GetBlockAccessList(ctx context.Context, block rpc.BlockNumberOrHash) ([]ethclient.BlockAccessListEntry, error) { + s.calls <- block + return []ethclient.BlockAccessListEntry{ + { + Address: common.HexToAddress("0x000f3df6d732807ef1319fb7b8bb8522d0beac02"), + BalanceChanges: []ethclient.BalanceChangeEntry{}, + CodeChanges: []ethclient.CodeChangeEntry{}, + NonceChanges: []ethclient.NonceChangeEntry{}, + StorageChanges: []ethclient.StorageChangesEntry{ + { + Key: common.HexToHash("0x152f"), + Changes: []ethclient.StorageChangeEntry{ + {Index: 0, Value: common.HexToHash("0x01")}, + }, + }, + }, + StorageReads: []common.Hash{}, + }, + { + Address: common.HexToAddress("0x8943545177806ed17b9f23f0a21ee5948ecaa776"), + BalanceChanges: []ethclient.BalanceChangeEntry{ + {Index: 1, Value: (*hexutil.Big)(big.NewInt(1e18))}, + }, + CodeChanges: []ethclient.CodeChangeEntry{}, + NonceChanges: []ethclient.NonceChangeEntry{{Index: 1, Value: 1}}, + StorageChanges: []ethclient.StorageChangesEntry{}, + StorageReads: []common.Hash{}, + }, + }, nil +} + +func TestGetBlockAccessList(t *testing.T) { + srv := rpc.NewServer() + service := &blockAccessListTestService{calls: make(chan rpc.BlockNumberOrHash, 1)} + if err := srv.RegisterName("eth", service); err != nil { + t.Fatalf("failed to register service: %v", err) + } + defer srv.Stop() + + client := rpc.DialInProc(srv) + defer client.Close() + + ec := ethclient.NewClient(client) + defer ec.Close() + + hash := common.HexToHash("0x01") + ref := rpc.BlockNumberOrHashWithHash(hash, true) + + entries, err := ec.GetBlockAccessList(context.Background(), ref) + if err != nil { + t.Fatalf("GetBlockAccessList returned error: %v", err) + } + if len(entries) != 2 { + t.Fatalf("expected 2 entries, got %d", len(entries)) + } + if entries[0].Address != common.HexToAddress("0x000f3df6d732807ef1319fb7b8bb8522d0beac02") { + t.Fatalf("unexpected first entry address: %s", entries[0].Address) + } + if len(entries[0].StorageChanges) != 1 || entries[0].StorageChanges[0].Changes[0].Index != 0 { + t.Fatalf("unexpected storage changes: %+v", entries[0].StorageChanges) + } + if entries[1].BalanceChanges[0].Value.ToInt().Cmp(big.NewInt(1e18)) != 0 { + t.Fatalf("unexpected balance change: %v", entries[1].BalanceChanges[0].Value) + } + if entries[1].NonceChanges[0].Value != 1 { + t.Fatalf("unexpected nonce change: %+v", entries[1].NonceChanges) + } +}