mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-12 09:51:36 +00:00
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
This commit is contained in:
parent
00da4f51ff
commit
3d8e0076d4
8 changed files with 184 additions and 6 deletions
|
|
@ -23,6 +23,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
type accessList struct {
|
||||
|
|
@ -143,6 +144,26 @@ func (al *accessList) Equal(other *accessList) bool {
|
|||
return slices.EqualFunc(al.slots, other.slots, maps.Equal)
|
||||
}
|
||||
|
||||
// export converts the internal access list into a types.AccessList.
|
||||
// The result is sorted by address and storage keys for deterministic output.
|
||||
func (al *accessList) export() types.AccessList {
|
||||
result := make(types.AccessList, 0, len(al.addresses))
|
||||
sortedAddrs := slices.Collect(maps.Keys(al.addresses))
|
||||
slices.SortFunc(sortedAddrs, common.Address.Cmp)
|
||||
for _, addr := range sortedAddrs {
|
||||
idx := al.addresses[addr]
|
||||
tuple := types.AccessTuple{Address: addr, StorageKeys: []common.Hash{}}
|
||||
if idx >= 0 {
|
||||
keys := slices.SortedFunc(maps.Keys(al.slots[idx]), common.Hash.Cmp)
|
||||
if keys != nil {
|
||||
tuple.StorageKeys = keys
|
||||
}
|
||||
}
|
||||
result = append(result, tuple)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PrettyPrint prints the contents of the access list in a human-readable form
|
||||
func (al *accessList) PrettyPrint() string {
|
||||
out := new(strings.Builder)
|
||||
|
|
|
|||
|
|
@ -1437,6 +1437,11 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre
|
|||
return s.accessList.Contains(addr, slot)
|
||||
}
|
||||
|
||||
// AccessList returns the current access list as a types.AccessList snapshot.
|
||||
func (s *StateDB) AccessList() types.AccessList {
|
||||
return s.accessList.export()
|
||||
}
|
||||
|
||||
// markDelete is invoked when an account is deleted but the deletion is
|
||||
// not yet committed. The pending mutation is cached and will be applied
|
||||
// all together
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ func (s *hookedStateDB) GetRefund() uint64 {
|
|||
return s.inner.GetRefund()
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) AccessList() types.AccessList {
|
||||
return s.inner.AccessList()
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) GetStateAndCommittedState(addr common.Address, hash common.Hash) (common.Hash, common.Hash) {
|
||||
return s.inner.GetStateAndCommittedState(addr, hash)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ type StateDB interface {
|
|||
GetTransientState(common.Address, common.Hash) common.Hash
|
||||
Exist(common.Address) bool
|
||||
GetRefund() uint64
|
||||
AccessList() types.AccessList
|
||||
}
|
||||
|
||||
// VMContext provides the context for the EVM execution.
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ type StateDB interface {
|
|||
// AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform
|
||||
// even if the feature/fork is not active yet
|
||||
AddSlotToAccessList(addr common.Address, slot common.Hash)
|
||||
// AccessList returns the current access list as a snapshot.
|
||||
AccessList() types.AccessList
|
||||
|
||||
Prepare(rules params.Rules, sender, coinbase common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/common/math"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
|
@ -26,6 +27,7 @@ func (s StructLog) MarshalJSON() ([]byte, error) {
|
|||
Stack []hexutil.U256 `json:"stack"`
|
||||
ReturnData hexutil.Bytes `json:"returnData,omitempty"`
|
||||
Storage map[common.Hash]common.Hash `json:"-"`
|
||||
AccessList types.AccessList `json:"accessList,omitempty"`
|
||||
Depth int `json:"depth"`
|
||||
RefundCounter uint64 `json:"refund"`
|
||||
Err error `json:"-"`
|
||||
|
|
@ -47,6 +49,7 @@ func (s StructLog) MarshalJSON() ([]byte, error) {
|
|||
}
|
||||
enc.ReturnData = s.ReturnData
|
||||
enc.Storage = s.Storage
|
||||
enc.AccessList = s.AccessList
|
||||
enc.Depth = s.Depth
|
||||
enc.RefundCounter = s.RefundCounter
|
||||
enc.Err = s.Err
|
||||
|
|
@ -67,6 +70,7 @@ func (s *StructLog) UnmarshalJSON(input []byte) error {
|
|||
Stack []hexutil.U256 `json:"stack"`
|
||||
ReturnData *hexutil.Bytes `json:"returnData,omitempty"`
|
||||
Storage map[common.Hash]common.Hash `json:"-"`
|
||||
AccessList *types.AccessList `json:"accessList,omitempty"`
|
||||
Depth *int `json:"depth"`
|
||||
RefundCounter *uint64 `json:"refund"`
|
||||
Err error `json:"-"`
|
||||
|
|
@ -105,6 +109,9 @@ func (s *StructLog) UnmarshalJSON(input []byte) error {
|
|||
if dec.Storage != nil {
|
||||
s.Storage = dec.Storage
|
||||
}
|
||||
if dec.AccessList != nil {
|
||||
s.AccessList = *dec.AccessList
|
||||
}
|
||||
if dec.Depth != nil {
|
||||
s.Depth = *dec.Depth
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,13 +40,39 @@ import (
|
|||
// Storage represents a contract's storage.
|
||||
type Storage map[common.Hash]common.Hash
|
||||
|
||||
// AccessListMode represents the capture mode for access lists in tracing.
|
||||
type AccessListMode string
|
||||
|
||||
const (
|
||||
AccessListModeDisabled AccessListMode = "" // default, no access list capture
|
||||
AccessListModeFull AccessListMode = "full" // capture full access list at each step
|
||||
)
|
||||
|
||||
// UnmarshalJSON parses the access list mode from JSON input.
|
||||
func (m *AccessListMode) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
switch s {
|
||||
case "", "disabled":
|
||||
*m = AccessListModeDisabled
|
||||
case "full":
|
||||
*m = AccessListModeFull
|
||||
default:
|
||||
return fmt.Errorf("unknown access list mode %q, want \"disabled\" or \"full\"", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config are the configuration options for structured logger the EVM
|
||||
type Config struct {
|
||||
EnableMemory bool // enable memory capture
|
||||
DisableStack bool // disable stack capture
|
||||
DisableStorage bool // disable storage capture
|
||||
EnableReturnData bool // enable return data capture
|
||||
Limit int // maximum size of output, but zero means unlimited
|
||||
EnableMemory bool // enable memory capture
|
||||
DisableStack bool // disable stack capture
|
||||
DisableStorage bool // disable storage capture
|
||||
EnableReturnData bool // enable return data capture
|
||||
AccessListMode AccessListMode // access list capture mode (default: disabled)
|
||||
Limit int // maximum size of output, but zero means unlimited
|
||||
// Chain overrides, can be used to execute a trace using future fork rules
|
||||
Overrides *params.ChainConfig `json:"overrides,omitempty"`
|
||||
}
|
||||
|
|
@ -65,6 +91,7 @@ type StructLog struct {
|
|||
Stack []uint256.Int `json:"stack"`
|
||||
ReturnData []byte `json:"returnData,omitempty"`
|
||||
Storage map[common.Hash]common.Hash `json:"-"`
|
||||
AccessList types.AccessList `json:"accessList,omitempty"`
|
||||
Depth int `json:"depth"`
|
||||
RefundCounter uint64 `json:"refund"`
|
||||
Err error `json:"-"`
|
||||
|
|
@ -153,6 +180,7 @@ type structLogLegacy struct {
|
|||
ReturnData string `json:"returnData,omitempty"`
|
||||
Memory *[]string `json:"memory,omitempty"`
|
||||
Storage *map[string]string `json:"storage,omitempty"`
|
||||
AccessList types.AccessList `json:"accessList,omitempty"`
|
||||
RefundCounter uint64 `json:"refund,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -204,6 +232,9 @@ func (s *StructLog) toLegacyJSON() json.RawMessage {
|
|||
}
|
||||
msg.Storage = &storage
|
||||
}
|
||||
if len(s.AccessList) > 0 {
|
||||
msg.AccessList = s.AccessList
|
||||
}
|
||||
element, _ := json.Marshal(msg)
|
||||
return element
|
||||
}
|
||||
|
|
@ -287,7 +318,16 @@ func (l *StructLogger) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope
|
|||
stack = scope.StackData()
|
||||
stackLen = len(stack)
|
||||
)
|
||||
log := StructLog{pc, op, gas, cost, nil, len(memory), nil, nil, nil, depth, l.env.StateDB.GetRefund(), err}
|
||||
log := StructLog{
|
||||
Pc: pc,
|
||||
Op: op,
|
||||
Gas: gas,
|
||||
GasCost: cost,
|
||||
MemorySize: len(memory),
|
||||
Depth: depth,
|
||||
RefundCounter: l.env.StateDB.GetRefund(),
|
||||
Err: err,
|
||||
}
|
||||
if l.cfg.EnableMemory {
|
||||
log.Memory = memory
|
||||
}
|
||||
|
|
@ -297,6 +337,11 @@ func (l *StructLogger) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope
|
|||
if l.cfg.EnableReturnData {
|
||||
log.ReturnData = rData
|
||||
}
|
||||
if l.cfg.AccessListMode == AccessListModeFull {
|
||||
// TODO: export() sorts and allocates on every opcode step, even when the
|
||||
// access list hasn't changed. This may cause GC pressure on long traces.
|
||||
log.AccessList = l.env.StateDB.AccessList()
|
||||
}
|
||||
|
||||
// Copy a snapshot of the current storage to a new container
|
||||
var storage Storage
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/holiman/uint256"
|
||||
|
|
@ -43,6 +44,15 @@ func (*dummyStatedb) GetStateAndCommittedState(common.Address, common.Hash) (com
|
|||
return common.Hash{}, common.Hash{}
|
||||
}
|
||||
|
||||
func (*dummyStatedb) AccessList() types.AccessList {
|
||||
return types.AccessList{
|
||||
{
|
||||
Address: common.HexToAddress("0xaaaa"),
|
||||
StorageKeys: []common.Hash{common.HexToHash("0x01")},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreCapture(t *testing.T) {
|
||||
var (
|
||||
logger = NewStructLogger(nil)
|
||||
|
|
@ -97,6 +107,89 @@ func TestStructLogMarshalingOmitEmpty(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAccessListCapture(t *testing.T) {
|
||||
type resultLog struct {
|
||||
AccessList types.AccessList `json:"accessList,omitempty"`
|
||||
}
|
||||
type execResult struct {
|
||||
StructLogs []resultLog `json:"structLogs"`
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mode AccessListMode
|
||||
wantCapture bool
|
||||
}{
|
||||
{"disabled by default", AccessListModeDisabled, false},
|
||||
{"full mode", AccessListModeFull, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger := NewStructLogger(&Config{AccessListMode: tt.mode})
|
||||
evm := vm.NewEVM(vm.BlockContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()})
|
||||
contract := vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), 100000, nil)
|
||||
contract.Code = []byte{byte(vm.PUSH1), 0x0, byte(vm.SLOAD)}
|
||||
|
||||
logger.OnTxStart(evm.GetVMContext(), nil, common.Address{})
|
||||
_, err := evm.Run(contract, []byte{}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
blob, err := logger.GetResult()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var result execResult
|
||||
if err := json.Unmarshal(blob, &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.StructLogs) == 0 {
|
||||
t.Fatal("expected at least one struct log")
|
||||
}
|
||||
for _, log := range result.StructLogs {
|
||||
if tt.wantCapture && log.AccessList == nil {
|
||||
t.Fatal("expected access list to be captured, got nil")
|
||||
}
|
||||
if !tt.wantCapture && log.AccessList != nil {
|
||||
t.Fatal("expected no access list capture, got non-nil")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessListModeUnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want AccessListMode
|
||||
wantErr bool
|
||||
}{
|
||||
{`""`, AccessListModeDisabled, false},
|
||||
{`"disabled"`, AccessListModeDisabled, false},
|
||||
{`"full"`, AccessListModeFull, false},
|
||||
{`"invalid"`, "", true},
|
||||
{`"Full"`, "", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
var mode AccessListMode
|
||||
err := json.Unmarshal([]byte(tt.input), &mode)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if mode != tt.want {
|
||||
t.Fatalf("got %q, want %q", mode, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructLogLegacyJSONSpecFormatting(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
Loading…
Reference in a new issue