mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-16 20:01:37 +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"
|
"strings"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type accessList struct {
|
type accessList struct {
|
||||||
|
|
@ -143,6 +144,26 @@ func (al *accessList) Equal(other *accessList) bool {
|
||||||
return slices.EqualFunc(al.slots, other.slots, maps.Equal)
|
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
|
// PrettyPrint prints the contents of the access list in a human-readable form
|
||||||
func (al *accessList) PrettyPrint() string {
|
func (al *accessList) PrettyPrint() string {
|
||||||
out := new(strings.Builder)
|
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)
|
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
|
// markDelete is invoked when an account is deleted but the deletion is
|
||||||
// not yet committed. The pending mutation is cached and will be applied
|
// not yet committed. The pending mutation is cached and will be applied
|
||||||
// all together
|
// all together
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,10 @@ func (s *hookedStateDB) GetRefund() uint64 {
|
||||||
return s.inner.GetRefund()
|
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) {
|
func (s *hookedStateDB) GetStateAndCommittedState(addr common.Address, hash common.Hash) (common.Hash, common.Hash) {
|
||||||
return s.inner.GetStateAndCommittedState(addr, hash)
|
return s.inner.GetStateAndCommittedState(addr, hash)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ type StateDB interface {
|
||||||
GetTransientState(common.Address, common.Hash) common.Hash
|
GetTransientState(common.Address, common.Hash) common.Hash
|
||||||
Exist(common.Address) bool
|
Exist(common.Address) bool
|
||||||
GetRefund() uint64
|
GetRefund() uint64
|
||||||
|
AccessList() types.AccessList
|
||||||
}
|
}
|
||||||
|
|
||||||
// VMContext provides the context for the EVM execution.
|
// 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
|
// 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
|
// even if the feature/fork is not active yet
|
||||||
AddSlotToAccessList(addr common.Address, slot common.Hash)
|
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)
|
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"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
"github.com/ethereum/go-ethereum/common/math"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
)
|
)
|
||||||
|
|
@ -26,6 +27,7 @@ func (s StructLog) MarshalJSON() ([]byte, error) {
|
||||||
Stack []hexutil.U256 `json:"stack"`
|
Stack []hexutil.U256 `json:"stack"`
|
||||||
ReturnData hexutil.Bytes `json:"returnData,omitempty"`
|
ReturnData hexutil.Bytes `json:"returnData,omitempty"`
|
||||||
Storage map[common.Hash]common.Hash `json:"-"`
|
Storage map[common.Hash]common.Hash `json:"-"`
|
||||||
|
AccessList types.AccessList `json:"accessList,omitempty"`
|
||||||
Depth int `json:"depth"`
|
Depth int `json:"depth"`
|
||||||
RefundCounter uint64 `json:"refund"`
|
RefundCounter uint64 `json:"refund"`
|
||||||
Err error `json:"-"`
|
Err error `json:"-"`
|
||||||
|
|
@ -47,6 +49,7 @@ func (s StructLog) MarshalJSON() ([]byte, error) {
|
||||||
}
|
}
|
||||||
enc.ReturnData = s.ReturnData
|
enc.ReturnData = s.ReturnData
|
||||||
enc.Storage = s.Storage
|
enc.Storage = s.Storage
|
||||||
|
enc.AccessList = s.AccessList
|
||||||
enc.Depth = s.Depth
|
enc.Depth = s.Depth
|
||||||
enc.RefundCounter = s.RefundCounter
|
enc.RefundCounter = s.RefundCounter
|
||||||
enc.Err = s.Err
|
enc.Err = s.Err
|
||||||
|
|
@ -67,6 +70,7 @@ func (s *StructLog) UnmarshalJSON(input []byte) error {
|
||||||
Stack []hexutil.U256 `json:"stack"`
|
Stack []hexutil.U256 `json:"stack"`
|
||||||
ReturnData *hexutil.Bytes `json:"returnData,omitempty"`
|
ReturnData *hexutil.Bytes `json:"returnData,omitempty"`
|
||||||
Storage map[common.Hash]common.Hash `json:"-"`
|
Storage map[common.Hash]common.Hash `json:"-"`
|
||||||
|
AccessList *types.AccessList `json:"accessList,omitempty"`
|
||||||
Depth *int `json:"depth"`
|
Depth *int `json:"depth"`
|
||||||
RefundCounter *uint64 `json:"refund"`
|
RefundCounter *uint64 `json:"refund"`
|
||||||
Err error `json:"-"`
|
Err error `json:"-"`
|
||||||
|
|
@ -105,6 +109,9 @@ func (s *StructLog) UnmarshalJSON(input []byte) error {
|
||||||
if dec.Storage != nil {
|
if dec.Storage != nil {
|
||||||
s.Storage = dec.Storage
|
s.Storage = dec.Storage
|
||||||
}
|
}
|
||||||
|
if dec.AccessList != nil {
|
||||||
|
s.AccessList = *dec.AccessList
|
||||||
|
}
|
||||||
if dec.Depth != nil {
|
if dec.Depth != nil {
|
||||||
s.Depth = *dec.Depth
|
s.Depth = *dec.Depth
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,39 @@ import (
|
||||||
// Storage represents a contract's storage.
|
// Storage represents a contract's storage.
|
||||||
type Storage map[common.Hash]common.Hash
|
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
|
// Config are the configuration options for structured logger the EVM
|
||||||
type Config struct {
|
type Config struct {
|
||||||
EnableMemory bool // enable memory capture
|
EnableMemory bool // enable memory capture
|
||||||
DisableStack bool // disable stack capture
|
DisableStack bool // disable stack capture
|
||||||
DisableStorage bool // disable storage capture
|
DisableStorage bool // disable storage capture
|
||||||
EnableReturnData bool // enable return data capture
|
EnableReturnData bool // enable return data capture
|
||||||
Limit int // maximum size of output, but zero means unlimited
|
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
|
// Chain overrides, can be used to execute a trace using future fork rules
|
||||||
Overrides *params.ChainConfig `json:"overrides,omitempty"`
|
Overrides *params.ChainConfig `json:"overrides,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +91,7 @@ type StructLog struct {
|
||||||
Stack []uint256.Int `json:"stack"`
|
Stack []uint256.Int `json:"stack"`
|
||||||
ReturnData []byte `json:"returnData,omitempty"`
|
ReturnData []byte `json:"returnData,omitempty"`
|
||||||
Storage map[common.Hash]common.Hash `json:"-"`
|
Storage map[common.Hash]common.Hash `json:"-"`
|
||||||
|
AccessList types.AccessList `json:"accessList,omitempty"`
|
||||||
Depth int `json:"depth"`
|
Depth int `json:"depth"`
|
||||||
RefundCounter uint64 `json:"refund"`
|
RefundCounter uint64 `json:"refund"`
|
||||||
Err error `json:"-"`
|
Err error `json:"-"`
|
||||||
|
|
@ -153,6 +180,7 @@ type structLogLegacy struct {
|
||||||
ReturnData string `json:"returnData,omitempty"`
|
ReturnData string `json:"returnData,omitempty"`
|
||||||
Memory *[]string `json:"memory,omitempty"`
|
Memory *[]string `json:"memory,omitempty"`
|
||||||
Storage *map[string]string `json:"storage,omitempty"`
|
Storage *map[string]string `json:"storage,omitempty"`
|
||||||
|
AccessList types.AccessList `json:"accessList,omitempty"`
|
||||||
RefundCounter uint64 `json:"refund,omitempty"`
|
RefundCounter uint64 `json:"refund,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,6 +232,9 @@ func (s *StructLog) toLegacyJSON() json.RawMessage {
|
||||||
}
|
}
|
||||||
msg.Storage = &storage
|
msg.Storage = &storage
|
||||||
}
|
}
|
||||||
|
if len(s.AccessList) > 0 {
|
||||||
|
msg.AccessList = s.AccessList
|
||||||
|
}
|
||||||
element, _ := json.Marshal(msg)
|
element, _ := json.Marshal(msg)
|
||||||
return element
|
return element
|
||||||
}
|
}
|
||||||
|
|
@ -287,7 +318,16 @@ func (l *StructLogger) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope
|
||||||
stack = scope.StackData()
|
stack = scope.StackData()
|
||||||
stackLen = len(stack)
|
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 {
|
if l.cfg.EnableMemory {
|
||||||
log.Memory = memory
|
log.Memory = memory
|
||||||
}
|
}
|
||||||
|
|
@ -297,6 +337,11 @@ func (l *StructLogger) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scope
|
||||||
if l.cfg.EnableReturnData {
|
if l.cfg.EnableReturnData {
|
||||||
log.ReturnData = rData
|
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
|
// Copy a snapshot of the current storage to a new container
|
||||||
var storage Storage
|
var storage Storage
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/state"
|
"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/core/vm"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
|
|
@ -43,6 +44,15 @@ func (*dummyStatedb) GetStateAndCommittedState(common.Address, common.Hash) (com
|
||||||
return common.Hash{}, common.Hash{}
|
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) {
|
func TestStoreCapture(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
logger = NewStructLogger(nil)
|
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) {
|
func TestStructLogLegacyJSONSpecFormatting(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue