Skip to content

Commit 3d8e007

Browse files
committed
core/state, eth/tracers: add access list to struct logger
Add AccessListMode option to the struct logger configuration. When set to "full", each StructLog entry includes a snapshot of the current access list at that execution step. Ref #25278
1 parent 00da4f5 commit 3d8e007

File tree

8 files changed

+184
-6
lines changed

8 files changed

+184
-6
lines changed

core/state/access_list.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"strings"
2424

2525
"github.com/ethereum/go-ethereum/common"
26+
"github.com/ethereum/go-ethereum/core/types"
2627
)
2728

2829
type accessList struct {
@@ -143,6 +144,26 @@ func (al *accessList) Equal(other *accessList) bool {
143144
return slices.EqualFunc(al.slots, other.slots, maps.Equal)
144145
}
145146

147+
// export converts the internal access list into a types.AccessList.
148+
// The result is sorted by address and storage keys for deterministic output.
149+
func (al *accessList) export() types.AccessList {
150+
result := make(types.AccessList, 0, len(al.addresses))
151+
sortedAddrs := slices.Collect(maps.Keys(al.addresses))
152+
slices.SortFunc(sortedAddrs, common.Address.Cmp)
153+
for _, addr := range sortedAddrs {
154+
idx := al.addresses[addr]
155+
tuple := types.AccessTuple{Address: addr, StorageKeys: []common.Hash{}}
156+
if idx >= 0 {
157+
keys := slices.SortedFunc(maps.Keys(al.slots[idx]), common.Hash.Cmp)
158+
if keys != nil {
159+
tuple.StorageKeys = keys
160+
}
161+
}
162+
result = append(result, tuple)
163+
}
164+
return result
165+
}
166+
146167
// PrettyPrint prints the contents of the access list in a human-readable form
147168
func (al *accessList) PrettyPrint() string {
148169
out := new(strings.Builder)

core/state/statedb.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,11 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre
14371437
return s.accessList.Contains(addr, slot)
14381438
}
14391439

1440+
// AccessList returns the current access list as a types.AccessList snapshot.
1441+
func (s *StateDB) AccessList() types.AccessList {
1442+
return s.accessList.export()
1443+
}
1444+
14401445
// markDelete is invoked when an account is deleted but the deletion is
14411446
// not yet committed. The pending mutation is cached and will be applied
14421447
// all together

core/state/statedb_hooked.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ func (s *hookedStateDB) GetRefund() uint64 {
9090
return s.inner.GetRefund()
9191
}
9292

93+
func (s *hookedStateDB) AccessList() types.AccessList {
94+
return s.inner.AccessList()
95+
}
96+
9397
func (s *hookedStateDB) GetStateAndCommittedState(addr common.Address, hash common.Hash) (common.Hash, common.Hash) {
9498
return s.inner.GetStateAndCommittedState(addr, hash)
9599
}

core/tracing/hooks.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type StateDB interface {
5656
GetTransientState(common.Address, common.Hash) common.Hash
5757
Exist(common.Address) bool
5858
GetRefund() uint64
59+
AccessList() types.AccessList
5960
}
6061

6162
// VMContext provides the context for the EVM execution.

core/vm/interface.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ type StateDB interface {
8080
// AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform
8181
// even if the feature/fork is not active yet
8282
AddSlotToAccessList(addr common.Address, slot common.Hash)
83+
// AccessList returns the current access list as a snapshot.
84+
AccessList() types.AccessList
8385

8486
Prepare(rules params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList)
8587

eth/tracers/logger/gen_structlog.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

eth/tracers/logger/logger.go

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,39 @@ import (
4040
// Storage represents a contract's storage.
4141
type Storage map[common.Hash]common.Hash
4242

43+
// AccessListMode represents the capture mode for access lists in tracing.
44+
type AccessListMode string
45+
46+
const (
47+
AccessListModeDisabled AccessListMode = "" // default, no access list capture
48+
AccessListModeFull AccessListMode = "full" // capture full access list at each step
49+
)
50+
51+
// UnmarshalJSON parses the access list mode from JSON input.
52+
func (m *AccessListMode) UnmarshalJSON(data []byte) error {
53+
var s string
54+
if err := json.Unmarshal(data, &s); err != nil {
55+
return err
56+
}
57+
switch s {
58+
case "", "disabled":
59+
*m = AccessListModeDisabled
60+
case "full":
61+
*m = AccessListModeFull
62+
default:
63+
return fmt.Errorf("unknown access list mode %q, want \"disabled\" or \"full\"", s)
64+
}
65+
return nil
66+
}
67+
4368
// Config are the configuration options for structured logger the EVM
4469
type Config struct {
45-
EnableMemory bool // enable memory capture
46-
DisableStack bool // disable stack capture
47-
DisableStorage bool // disable storage capture
48-
EnableReturnData bool // enable return data capture
49-
Limit int // maximum size of output, but zero means unlimited
70+
EnableMemory bool // enable memory capture
71+
DisableStack bool // disable stack capture
72+
DisableStorage bool // disable storage capture
73+
EnableReturnData bool // enable return data capture
74+
AccessListMode AccessListMode // access list capture mode (default: disabled)
75+
Limit int // maximum size of output, but zero means unlimited
5076
// Chain overrides, can be used to execute a trace using future fork rules
5177
Overrides *params.ChainConfig `json:"overrides,omitempty"`
5278
}
@@ -65,6 +91,7 @@ type StructLog struct {
6591
Stack []uint256.Int `json:"stack"`
6692
ReturnData []byte `json:"returnData,omitempty"`
6793
Storage map[common.Hash]common.Hash `json:"-"`
94+
AccessList types.AccessList `json:"accessList,omitempty"`
6895
Depth int `json:"depth"`
6996
RefundCounter uint64 `json:"refund"`
7097
Err error `json:"-"`
@@ -153,6 +180,7 @@ type structLogLegacy struct {
153180
ReturnData string `json:"returnData,omitempty"`
154181
Memory *[]string `json:"memory,omitempty"`
155182
Storage *map[string]string `json:"storage,omitempty"`
183+
AccessList types.AccessList `json:"accessList,omitempty"`
156184
RefundCounter uint64 `json:"refund,omitempty"`
157185
}
158186

@@ -204,6 +232,9 @@ func (s *StructLog) toLegacyJSON() json.RawMessage {
204232
}
205233
msg.Storage = &storage
206234
}
235+
if len(s.AccessList) > 0 {
236+
msg.AccessList = s.AccessList
237+
}
207238
element, _ := json.Marshal(msg)
208239
return element
209240
}
@@ -287,7 +318,16 @@ func (l *StructLogger) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope
287318
stack = scope.StackData()
288319
stackLen = len(stack)
289320
)
290-
log := StructLog{pc, op, gas, cost, nil, len(memory), nil, nil, nil, depth, l.env.StateDB.GetRefund(), err}
321+
log := StructLog{
322+
Pc: pc,
323+
Op: op,
324+
Gas: gas,
325+
GasCost: cost,
326+
MemorySize: len(memory),
327+
Depth: depth,
328+
RefundCounter: l.env.StateDB.GetRefund(),
329+
Err: err,
330+
}
291331
if l.cfg.EnableMemory {
292332
log.Memory = memory
293333
}
@@ -297,6 +337,11 @@ func (l *StructLogger) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope
297337
if l.cfg.EnableReturnData {
298338
log.ReturnData = rData
299339
}
340+
if l.cfg.AccessListMode == AccessListModeFull {
341+
// TODO: export() sorts and allocates on every opcode step, even when the
342+
// access list hasn't changed. This may cause GC pressure on long traces.
343+
log.AccessList = l.env.StateDB.AccessList()
344+
}
300345

301346
// Copy a snapshot of the current storage to a new container
302347
var storage Storage

eth/tracers/logger/logger_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/ethereum/go-ethereum/common"
2626
"github.com/ethereum/go-ethereum/core/state"
27+
"github.com/ethereum/go-ethereum/core/types"
2728
"github.com/ethereum/go-ethereum/core/vm"
2829
"github.com/ethereum/go-ethereum/params"
2930
"github.com/holiman/uint256"
@@ -43,6 +44,15 @@ func (*dummyStatedb) GetStateAndCommittedState(common.Address, common.Hash) (com
4344
return common.Hash{}, common.Hash{}
4445
}
4546

47+
func (*dummyStatedb) AccessList() types.AccessList {
48+
return types.AccessList{
49+
{
50+
Address: common.HexToAddress("0xaaaa"),
51+
StorageKeys: []common.Hash{common.HexToHash("0x01")},
52+
},
53+
}
54+
}
55+
4656
func TestStoreCapture(t *testing.T) {
4757
var (
4858
logger = NewStructLogger(nil)
@@ -97,6 +107,89 @@ func TestStructLogMarshalingOmitEmpty(t *testing.T) {
97107
}
98108
}
99109

110+
func TestAccessListCapture(t *testing.T) {
111+
type resultLog struct {
112+
AccessList types.AccessList `json:"accessList,omitempty"`
113+
}
114+
type execResult struct {
115+
StructLogs []resultLog `json:"structLogs"`
116+
}
117+
118+
tests := []struct {
119+
name string
120+
mode AccessListMode
121+
wantCapture bool
122+
}{
123+
{"disabled by default", AccessListModeDisabled, false},
124+
{"full mode", AccessListModeFull, true},
125+
}
126+
for _, tt := range tests {
127+
t.Run(tt.name, func(t *testing.T) {
128+
logger := NewStructLogger(&Config{AccessListMode: tt.mode})
129+
evm := vm.NewEVM(vm.BlockContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()})
130+
contract := vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), 100000, nil)
131+
contract.Code = []byte{byte(vm.PUSH1), 0x0, byte(vm.SLOAD)}
132+
133+
logger.OnTxStart(evm.GetVMContext(), nil, common.Address{})
134+
_, err := evm.Run(contract, []byte{}, false)
135+
if err != nil {
136+
t.Fatal(err)
137+
}
138+
blob, err := logger.GetResult()
139+
if err != nil {
140+
t.Fatal(err)
141+
}
142+
var result execResult
143+
if err := json.Unmarshal(blob, &result); err != nil {
144+
t.Fatal(err)
145+
}
146+
if len(result.StructLogs) == 0 {
147+
t.Fatal("expected at least one struct log")
148+
}
149+
for _, log := range result.StructLogs {
150+
if tt.wantCapture && log.AccessList == nil {
151+
t.Fatal("expected access list to be captured, got nil")
152+
}
153+
if !tt.wantCapture && log.AccessList != nil {
154+
t.Fatal("expected no access list capture, got non-nil")
155+
}
156+
}
157+
})
158+
}
159+
}
160+
161+
func TestAccessListModeUnmarshalJSON(t *testing.T) {
162+
tests := []struct {
163+
input string
164+
want AccessListMode
165+
wantErr bool
166+
}{
167+
{`""`, AccessListModeDisabled, false},
168+
{`"disabled"`, AccessListModeDisabled, false},
169+
{`"full"`, AccessListModeFull, false},
170+
{`"invalid"`, "", true},
171+
{`"Full"`, "", true},
172+
}
173+
for _, tt := range tests {
174+
t.Run(tt.input, func(t *testing.T) {
175+
var mode AccessListMode
176+
err := json.Unmarshal([]byte(tt.input), &mode)
177+
if tt.wantErr {
178+
if err == nil {
179+
t.Fatal("expected error, got nil")
180+
}
181+
return
182+
}
183+
if err != nil {
184+
t.Fatalf("unexpected error: %v", err)
185+
}
186+
if mode != tt.want {
187+
t.Fatalf("got %q, want %q", mode, tt.want)
188+
}
189+
})
190+
}
191+
}
192+
100193
func TestStructLogLegacyJSONSpecFormatting(t *testing.T) {
101194
tests := []struct {
102195
name string

0 commit comments

Comments
 (0)