Skip to content

Commit 3703c08

Browse files
committed
ethapi: add eth_getBlockAccessList
1 parent 02b73d4 commit 3703c08

2 files changed

Lines changed: 304 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2026 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package ethapi
18+
19+
import (
20+
"context"
21+
22+
"github.com/ethereum/go-ethereum/common"
23+
"github.com/ethereum/go-ethereum/common/hexutil"
24+
"github.com/ethereum/go-ethereum/rpc"
25+
)
26+
27+
// The result types below mirror the execution-apis block access list schema:
28+
// each change entry pairs a 32-bit block-access index with the post-state
29+
// value, storage slots pair a 32-byte key with per-index writes, balance
30+
// values are 256-bit quantities and storage/read values are 32-byte ones.
31+
32+
type storageChangeResult struct {
33+
Index hexutil.Uint64 `json:"index"`
34+
Value common.Hash `json:"value"`
35+
}
36+
37+
type slotChangesResult struct {
38+
Key common.Hash `json:"key"`
39+
Changes []storageChangeResult `json:"changes"`
40+
}
41+
42+
type balanceChangeResult struct {
43+
Index hexutil.Uint64 `json:"index"`
44+
Value *hexutil.Big `json:"value"`
45+
}
46+
47+
type nonceChangeResult struct {
48+
Index hexutil.Uint64 `json:"index"`
49+
Value hexutil.Uint64 `json:"value"`
50+
}
51+
52+
type codeChangeResult struct {
53+
Index hexutil.Uint64 `json:"index"`
54+
Code hexutil.Bytes `json:"code"`
55+
}
56+
57+
type accountAccessResult struct {
58+
Address common.Address `json:"address"`
59+
BalanceChanges []balanceChangeResult `json:"balanceChanges"`
60+
CodeChanges []codeChangeResult `json:"codeChanges"`
61+
NonceChanges []nonceChangeResult `json:"nonceChanges"`
62+
StorageChanges []slotChangesResult `json:"storageChanges"`
63+
StorageReads []common.Hash `json:"storageReads"`
64+
}
65+
66+
// GetBlockAccessList returns the block access list for the given block.
67+
//
68+
// The list is a lexicographically sorted array of account accesses, each
69+
// describing the per-transaction post-state changes (balance, nonce, code,
70+
// storage writes) and the storage reads performed on the account during
71+
// block execution.
72+
//
73+
// Blocks predating the fork carrying the block access list (or otherwise
74+
// lacking one) return an empty list. A null result means the block does not
75+
// exist.
76+
func (api *BlockChainAPI) GetBlockAccessList(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]accountAccessResult, error) {
77+
block, err := api.b.BlockByNumberOrHash(ctx, blockNrOrHash)
78+
if block == nil || err != nil {
79+
return nil, err
80+
}
81+
al := block.AccessList()
82+
if al == nil {
83+
return []accountAccessResult{}, nil
84+
}
85+
out := make([]accountAccessResult, 0, len(*al))
86+
for _, acc := range *al {
87+
// The spec requires all change arrays to be present (possibly empty).
88+
res := accountAccessResult{
89+
Address: acc.Address,
90+
BalanceChanges: make([]balanceChangeResult, 0, len(acc.BalanceChanges)),
91+
NonceChanges: make([]nonceChangeResult, 0, len(acc.NonceChanges)),
92+
CodeChanges: make([]codeChangeResult, 0, len(acc.CodeChanges)),
93+
StorageChanges: make([]slotChangesResult, 0, len(acc.StorageChanges)),
94+
StorageReads: make([]common.Hash, 0, len(acc.StorageReads)),
95+
}
96+
97+
for _, change := range acc.BalanceChanges {
98+
res.BalanceChanges = append(res.BalanceChanges, balanceChangeResult{
99+
Index: hexutil.Uint64(change.BlockAccessIndex),
100+
Value: (*hexutil.Big)(change.PostBalance.ToBig()),
101+
})
102+
}
103+
for _, change := range acc.NonceChanges {
104+
res.NonceChanges = append(res.NonceChanges, nonceChangeResult{
105+
Index: hexutil.Uint64(change.BlockAccessIndex),
106+
Value: hexutil.Uint64(change.PostNonce),
107+
})
108+
}
109+
for _, change := range acc.CodeChanges {
110+
res.CodeChanges = append(res.CodeChanges, codeChangeResult{
111+
Index: hexutil.Uint64(change.BlockAccessIndex),
112+
Code: hexutil.Bytes(change.NewCode),
113+
})
114+
}
115+
for _, slot := range acc.StorageChanges {
116+
changes := make([]storageChangeResult, 0, len(slot.SlotChanges))
117+
for _, write := range slot.SlotChanges {
118+
var value [32]byte
119+
write.PostValue.WriteToSlice(value[:])
120+
changes = append(changes, storageChangeResult{
121+
Index: hexutil.Uint64(write.BlockAccessIndex),
122+
Value: common.Hash(value),
123+
})
124+
}
125+
var key [32]byte
126+
slot.Slot.WriteToSlice(key[:])
127+
res.StorageChanges = append(res.StorageChanges, slotChangesResult{
128+
Key: common.Hash(key),
129+
Changes: changes,
130+
})
131+
}
132+
for _, read := range acc.StorageReads {
133+
var key [32]byte
134+
read.WriteToSlice(key[:])
135+
res.StorageReads = append(res.StorageReads, common.Hash(key))
136+
}
137+
out = append(out, res)
138+
}
139+
return out, nil
140+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2026 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package ethapi
18+
19+
import (
20+
"bytes"
21+
"context"
22+
"encoding/json"
23+
"math/big"
24+
"testing"
25+
26+
"github.com/ethereum/go-ethereum/common"
27+
"github.com/ethereum/go-ethereum/consensus/beacon"
28+
"github.com/ethereum/go-ethereum/consensus/ethash"
29+
"github.com/ethereum/go-ethereum/core"
30+
"github.com/ethereum/go-ethereum/core/types"
31+
"github.com/ethereum/go-ethereum/params"
32+
"github.com/ethereum/go-ethereum/rpc"
33+
)
34+
35+
// TestGetBlockAccessList exercises eth_getBlockAccessList against a chain
36+
// with the Amsterdam fork active, asserting that the stored block access list
37+
// is returned for number, tag and hash lookups, and that the JSON encoding
38+
// follows the execution-apis spec field names.
39+
func TestGetBlockAccessList(t *testing.T) {
40+
t.Parallel()
41+
accounts := newAccounts(2)
42+
43+
cfg := *params.MergedTestChainConfig
44+
cfg.AmsterdamTime = uint64Ptr(0) // activate the block access list fork
45+
46+
genBlocks := 3
47+
api := NewBlockChainAPI(newTestBackend(t, genBlocks, &core.Genesis{
48+
Config: &cfg,
49+
Alloc: types.GenesisAlloc{accounts[0].addr: {Balance: big.NewInt(params.Ether)}},
50+
}, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) {
51+
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
52+
Nonce: uint64(i),
53+
To: &accounts[1].addr,
54+
Value: big.NewInt(1000),
55+
Gas: params.TxGas,
56+
GasPrice: b.BaseFee(),
57+
}), types.HomesteadSigner{}, accounts[0].key)
58+
b.AddTx(tx)
59+
b.SetPoS()
60+
}))
61+
62+
latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
63+
64+
// Resolve the latest block once for the by-hash lookup below.
65+
block, err := api.b.BlockByNumberOrHash(context.Background(), latest)
66+
if err != nil {
67+
t.Fatalf("failed to resolve latest block: %v", err)
68+
}
69+
70+
var testSuite = []struct {
71+
name string
72+
ref rpc.BlockNumberOrHash
73+
}{
74+
{"by-number", latest},
75+
{"by-hash", rpc.BlockNumberOrHashWithHash(block.Hash(), false)},
76+
}
77+
for _, tc := range testSuite {
78+
t.Run(tc.name, func(t *testing.T) {
79+
al, err := api.GetBlockAccessList(context.Background(), tc.ref)
80+
if err != nil {
81+
t.Fatalf("GetBlockAccessList error: %v", err)
82+
}
83+
if al == nil {
84+
t.Fatal("expected a non-nil access list")
85+
}
86+
if len(al) == 0 {
87+
t.Fatal("expected a non-empty access list for a block with transactions")
88+
}
89+
// The list is sorted lexicographically by address; the coinbase
90+
// (fee recipient) and both transfer accounts must be present.
91+
var found [3]bool
92+
want := []common.Address{{}, accounts[0].addr, accounts[1].addr}
93+
for i, entry := range al {
94+
if i > 0 && bytes.Compare(al[i-1].Address[:], entry.Address[:]) >= 0 {
95+
t.Fatalf("access list not sorted at index %d: %s >= %s", i, al[i-1].Address, entry.Address)
96+
}
97+
for j, addr := range want {
98+
if entry.Address == addr {
99+
found[j] = true
100+
}
101+
}
102+
}
103+
for j, addr := range want {
104+
if !found[j] {
105+
t.Fatalf("access list missing expected account %s", addr)
106+
}
107+
}
108+
})
109+
}
110+
111+
// The JSON encoding must follow the execution-apis spec field names.
112+
al, err := api.GetBlockAccessList(context.Background(), latest)
113+
if err != nil {
114+
t.Fatalf("GetBlockAccessList error: %v", err)
115+
}
116+
blob, err := json.Marshal(al)
117+
if err != nil {
118+
t.Fatalf("json.Marshal error: %v", err)
119+
}
120+
var decoded []map[string]any
121+
if err := json.Unmarshal(blob, &decoded); err != nil {
122+
t.Fatalf("json.Unmarshal error: %v", err)
123+
}
124+
for _, want := range []string{"address", "balanceChanges", "nonceChanges", "codeChanges", "storageChanges", "storageReads"} {
125+
if _, ok := decoded[0][want]; !ok {
126+
t.Fatalf("access list entry missing spec field %q: %s", want, blob)
127+
}
128+
}
129+
for _, field := range []string{"balanceChanges", "nonceChanges", "codeChanges", "storageChanges"} {
130+
if !hasSpecInnerFields(decoded[0][field]) {
131+
t.Fatalf("spec field %q has wrong inner shape: %s", field, blob)
132+
}
133+
}
134+
135+
// Unknown blocks must yield null, not an error.
136+
if al, err := api.GetBlockAccessList(context.Background(), rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(genBlocks+100))); al != nil || err != nil {
137+
t.Fatalf("expected null for unknown block, got list=%v err=%v", al, err)
138+
}
139+
}
140+
141+
// hasSpecInnerFields checks that the change array uses the execution-apis
142+
// index/value (or key/changes for storage) naming rather than the internal
143+
// encoding naming.
144+
func hasSpecInnerFields(v any) bool {
145+
entries, ok := v.([]any)
146+
if !ok || len(entries) == 0 {
147+
return true // empty arrays are fine
148+
}
149+
entry, ok := entries[0].(map[string]any)
150+
if !ok {
151+
return false
152+
}
153+
if _, ok := entry["index"]; ok {
154+
return true
155+
}
156+
if _, ok := entry["key"]; ok {
157+
if _, ok := entry["changes"]; ok {
158+
return true
159+
}
160+
}
161+
return false
162+
}
163+
164+
func uint64Ptr(v uint64) *uint64 { return &v }

0 commit comments

Comments
 (0)