Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions ethclient/block_access_list.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
}
74 changes: 74 additions & 0 deletions ethclient/ethclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
140 changes: 140 additions & 0 deletions internal/ethapi/block_access_list.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
}
Loading