mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-17 20:31:35 +00:00
eth/tracers/logger: fix legacy structLog JSON encoding
Fix several issues in the legacy structLog JSON format produced by the struct logger tracer: - Error field: change from string to *string so that error is truly omitted (rather than serialised as "") when there is no error. - Memory words: zero-pad each 32-byte chunk before hex-encoding so the output always matches the expected 64-character format instead of emitting a truncated hex string for partial words. - Storage/ReturnData: use hexutil.Encode / common.Hash.Hex() to produce consistent 0x-prefixed output rather than bare %x formatting. Add tests that cover the legacy JSON shape directly (TestStructLogLegacyJSONSpecFormatting), and two integration-level tracer tests verifying that refund counters and storage snapshots are captured correctly and that hard-failure vs revert return values are handled as specified.
This commit is contained in:
parent
8a3a309fa9
commit
c787778ed4
3 changed files with 238 additions and 5 deletions
|
|
@ -202,6 +202,18 @@ type stateTracer struct {
|
||||||
Storage map[common.Address]map[common.Hash]common.Hash
|
Storage map[common.Address]map[common.Hash]common.Hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tracedOpcodeLog struct {
|
||||||
|
Op string `json:"op"`
|
||||||
|
Refund *uint64 `json:"refund,omitempty"`
|
||||||
|
Storage map[string]string `json:"storage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tracedOpcodeResult struct {
|
||||||
|
Failed bool `json:"failed"`
|
||||||
|
ReturnValue string `json:"returnValue"`
|
||||||
|
StructLogs []tracedOpcodeLog `json:"structLogs"`
|
||||||
|
}
|
||||||
|
|
||||||
func newStateTracer(ctx *Context, cfg json.RawMessage, chainCfg *params.ChainConfig) (*Tracer, error) {
|
func newStateTracer(ctx *Context, cfg json.RawMessage, chainCfg *params.ChainConfig) (*Tracer, error) {
|
||||||
t := &stateTracer{
|
t := &stateTracer{
|
||||||
Balance: make(map[common.Address]*hexutil.Big),
|
Balance: make(map[common.Address]*hexutil.Big),
|
||||||
|
|
@ -1058,6 +1070,176 @@ func TestTracingWithOverrides(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTraceTransactionRefundAndStorageSnapshots(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
accounts := newAccounts(1)
|
||||||
|
contract := common.HexToAddress("0x00000000000000000000000000000000deadbeef")
|
||||||
|
slot0 := common.BigToHash(big.NewInt(0))
|
||||||
|
txSigner := types.HomesteadSigner{}
|
||||||
|
genesis := &core.Genesis{
|
||||||
|
Config: params.TestChainConfig,
|
||||||
|
Alloc: types.GenesisAlloc{
|
||||||
|
accounts[0].addr: {Balance: big.NewInt(params.Ether)},
|
||||||
|
contract: {
|
||||||
|
Nonce: 1,
|
||||||
|
Code: []byte{
|
||||||
|
byte(vm.PUSH1), 0x00,
|
||||||
|
byte(vm.SLOAD),
|
||||||
|
byte(vm.POP),
|
||||||
|
byte(vm.PUSH1), 0x00,
|
||||||
|
byte(vm.PUSH1), 0x00,
|
||||||
|
byte(vm.SSTORE),
|
||||||
|
byte(vm.STOP),
|
||||||
|
},
|
||||||
|
Storage: map[common.Hash]common.Hash{
|
||||||
|
slot0: common.BigToHash(big.NewInt(1)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var target common.Hash
|
||||||
|
backend := newTestBackend(t, 1, genesis, func(i int, b *core.BlockGen) {
|
||||||
|
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
|
||||||
|
Nonce: 0,
|
||||||
|
To: &contract,
|
||||||
|
Value: big.NewInt(0),
|
||||||
|
Gas: 100000,
|
||||||
|
GasPrice: b.BaseFee(),
|
||||||
|
}), txSigner, accounts[0].key)
|
||||||
|
b.AddTx(tx)
|
||||||
|
target = tx.Hash()
|
||||||
|
})
|
||||||
|
defer backend.teardown()
|
||||||
|
|
||||||
|
api := NewAPI(backend)
|
||||||
|
result, err := api.TraceTransaction(context.Background(), target, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to trace refunding transaction: %v", err)
|
||||||
|
}
|
||||||
|
var traced tracedOpcodeResult
|
||||||
|
if err := json.Unmarshal(result.(json.RawMessage), &traced); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal trace result: %v", err)
|
||||||
|
}
|
||||||
|
if traced.Failed {
|
||||||
|
t.Fatal("expected refunding transaction to succeed")
|
||||||
|
}
|
||||||
|
if traced.ReturnValue != "0x" {
|
||||||
|
t.Fatalf("unexpected return value: have %s want 0x", traced.ReturnValue)
|
||||||
|
}
|
||||||
|
slotHex := slot0.Hex()
|
||||||
|
oneHex := common.BigToHash(big.NewInt(1)).Hex()
|
||||||
|
zeroHex := common.Hash{}.Hex()
|
||||||
|
var (
|
||||||
|
foundSloadSnapshot bool
|
||||||
|
foundSstoreSnapshot bool
|
||||||
|
foundRefund bool
|
||||||
|
)
|
||||||
|
for _, log := range traced.StructLogs {
|
||||||
|
switch log.Op {
|
||||||
|
case "SLOAD":
|
||||||
|
if got := log.Storage[slotHex]; got == oneHex {
|
||||||
|
foundSloadSnapshot = true
|
||||||
|
}
|
||||||
|
case "SSTORE":
|
||||||
|
if got := log.Storage[slotHex]; got == zeroHex {
|
||||||
|
foundSstoreSnapshot = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if log.Refund != nil && *log.Refund > 0 {
|
||||||
|
foundRefund = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundSloadSnapshot {
|
||||||
|
t.Fatal("expected SLOAD snapshot to include the pre-existing non-zero storage value")
|
||||||
|
}
|
||||||
|
if !foundSstoreSnapshot {
|
||||||
|
t.Fatal("expected SSTORE snapshot to include the post-write zeroed storage value")
|
||||||
|
}
|
||||||
|
if !foundRefund {
|
||||||
|
t.Fatal("expected at least one structLog entry with a non-zero refund field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTraceTransactionFailureReturnValues(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
code []byte
|
||||||
|
wantReturnValue string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "revert preserves return data",
|
||||||
|
code: []byte{
|
||||||
|
byte(vm.PUSH1), 0x2a,
|
||||||
|
byte(vm.PUSH1), 0x00,
|
||||||
|
byte(vm.MSTORE),
|
||||||
|
byte(vm.PUSH1), 0x20,
|
||||||
|
byte(vm.PUSH1), 0x00,
|
||||||
|
byte(vm.REVERT),
|
||||||
|
},
|
||||||
|
wantReturnValue: "0x000000000000000000000000000000000000000000000000000000000000002a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hard failure clears return data",
|
||||||
|
code: []byte{
|
||||||
|
byte(vm.INVALID),
|
||||||
|
},
|
||||||
|
wantReturnValue: "0x",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
accounts := newAccounts(1)
|
||||||
|
contract := common.HexToAddress("0x00000000000000000000000000000000deadbeef")
|
||||||
|
txSigner := types.HomesteadSigner{}
|
||||||
|
genesis := &core.Genesis{
|
||||||
|
Config: params.TestChainConfig,
|
||||||
|
Alloc: types.GenesisAlloc{
|
||||||
|
accounts[0].addr: {Balance: big.NewInt(params.Ether)},
|
||||||
|
contract: {
|
||||||
|
Nonce: 1,
|
||||||
|
Code: tc.code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var target common.Hash
|
||||||
|
backend := newTestBackend(t, 1, genesis, func(i int, b *core.BlockGen) {
|
||||||
|
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
|
||||||
|
Nonce: 0,
|
||||||
|
To: &contract,
|
||||||
|
Value: big.NewInt(0),
|
||||||
|
Gas: 100000,
|
||||||
|
GasPrice: b.BaseFee(),
|
||||||
|
}), txSigner, accounts[0].key)
|
||||||
|
b.AddTx(tx)
|
||||||
|
target = tx.Hash()
|
||||||
|
})
|
||||||
|
defer backend.teardown()
|
||||||
|
|
||||||
|
api := NewAPI(backend)
|
||||||
|
result, err := api.TraceTransaction(context.Background(), target, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to trace transaction: %v", err)
|
||||||
|
}
|
||||||
|
var traced tracedOpcodeResult
|
||||||
|
if err := json.Unmarshal(result.(json.RawMessage), &traced); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal trace result: %v", err)
|
||||||
|
}
|
||||||
|
if !traced.Failed {
|
||||||
|
t.Fatal("expected traced transaction to fail")
|
||||||
|
}
|
||||||
|
if traced.ReturnValue != tc.wantReturnValue {
|
||||||
|
t.Fatalf("unexpected returnValue: have %s want %s", traced.ReturnValue, tc.wantReturnValue)
|
||||||
|
}
|
||||||
|
if len(traced.StructLogs) == 0 {
|
||||||
|
t.Fatal("expected failing trace to still include structLogs")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
key *ecdsa.PrivateKey
|
key *ecdsa.PrivateKey
|
||||||
addr common.Address
|
addr common.Address
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ type structLogLegacy struct {
|
||||||
Gas uint64 `json:"gas"`
|
Gas uint64 `json:"gas"`
|
||||||
GasCost uint64 `json:"gasCost"`
|
GasCost uint64 `json:"gasCost"`
|
||||||
Depth int `json:"depth"`
|
Depth int `json:"depth"`
|
||||||
Error string `json:"error,omitempty"`
|
Error *string `json:"error,omitempty"`
|
||||||
Stack *[]string `json:"stack,omitempty"`
|
Stack *[]string `json:"stack,omitempty"`
|
||||||
ReturnData string `json:"returnData,omitempty"`
|
ReturnData string `json:"returnData,omitempty"`
|
||||||
Memory *[]string `json:"memory,omitempty"`
|
Memory *[]string `json:"memory,omitempty"`
|
||||||
|
|
@ -156,6 +156,12 @@ type structLogLegacy struct {
|
||||||
RefundCounter uint64 `json:"refund,omitempty"`
|
RefundCounter uint64 `json:"refund,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatMemoryWord(chunk []byte) string {
|
||||||
|
var word [32]byte
|
||||||
|
copy(word[:], chunk)
|
||||||
|
return hexutil.Encode(word[:])
|
||||||
|
}
|
||||||
|
|
||||||
// toLegacyJSON converts the structLog to legacy json-encoded legacy form.
|
// toLegacyJSON converts the structLog to legacy json-encoded legacy form.
|
||||||
func (s *StructLog) toLegacyJSON() json.RawMessage {
|
func (s *StructLog) toLegacyJSON() json.RawMessage {
|
||||||
msg := structLogLegacy{
|
msg := structLogLegacy{
|
||||||
|
|
@ -164,9 +170,11 @@ func (s *StructLog) toLegacyJSON() json.RawMessage {
|
||||||
Gas: s.Gas,
|
Gas: s.Gas,
|
||||||
GasCost: s.GasCost,
|
GasCost: s.GasCost,
|
||||||
Depth: s.Depth,
|
Depth: s.Depth,
|
||||||
Error: s.ErrorString(),
|
|
||||||
RefundCounter: s.RefundCounter,
|
RefundCounter: s.RefundCounter,
|
||||||
}
|
}
|
||||||
|
if err := s.ErrorString(); err != "" {
|
||||||
|
msg.Error = &err
|
||||||
|
}
|
||||||
if s.Stack != nil {
|
if s.Stack != nil {
|
||||||
stack := make([]string, len(s.Stack))
|
stack := make([]string, len(s.Stack))
|
||||||
for i, stackValue := range s.Stack {
|
for i, stackValue := range s.Stack {
|
||||||
|
|
@ -175,7 +183,7 @@ func (s *StructLog) toLegacyJSON() json.RawMessage {
|
||||||
msg.Stack = &stack
|
msg.Stack = &stack
|
||||||
}
|
}
|
||||||
if len(s.ReturnData) > 0 {
|
if len(s.ReturnData) > 0 {
|
||||||
msg.ReturnData = hexutil.Bytes(s.ReturnData).String()
|
msg.ReturnData = hexutil.Encode(s.ReturnData)
|
||||||
}
|
}
|
||||||
if len(s.Memory) > 0 {
|
if len(s.Memory) > 0 {
|
||||||
memory := make([]string, 0, (len(s.Memory)+31)/32)
|
memory := make([]string, 0, (len(s.Memory)+31)/32)
|
||||||
|
|
@ -184,14 +192,14 @@ func (s *StructLog) toLegacyJSON() json.RawMessage {
|
||||||
if end > len(s.Memory) {
|
if end > len(s.Memory) {
|
||||||
end = len(s.Memory)
|
end = len(s.Memory)
|
||||||
}
|
}
|
||||||
memory = append(memory, fmt.Sprintf("%x", s.Memory[i:end]))
|
memory = append(memory, formatMemoryWord(s.Memory[i:end]))
|
||||||
}
|
}
|
||||||
msg.Memory = &memory
|
msg.Memory = &memory
|
||||||
}
|
}
|
||||||
if len(s.Storage) > 0 {
|
if len(s.Storage) > 0 {
|
||||||
storage := make(map[string]string)
|
storage := make(map[string]string)
|
||||||
for i, storageValue := range s.Storage {
|
for i, storageValue := range s.Storage {
|
||||||
storage[fmt.Sprintf("%x", i)] = fmt.Sprintf("%x", storageValue)
|
storage[i.Hex()] = storageValue.Hex()
|
||||||
}
|
}
|
||||||
msg.Storage = &storage
|
msg.Storage = &storage
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,3 +96,46 @@ func TestStructLogMarshalingOmitEmpty(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStructLogLegacyJSONSpecFormatting(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
log *StructLog
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "omits empty error and pads memory/storage",
|
||||||
|
log: &StructLog{
|
||||||
|
Pc: 7,
|
||||||
|
Op: vm.SSTORE,
|
||||||
|
Gas: 100,
|
||||||
|
GasCost: 20,
|
||||||
|
Memory: []byte{0xaa, 0xbb},
|
||||||
|
Storage: map[common.Hash]common.Hash{common.BigToHash(big.NewInt(1)): common.BigToHash(big.NewInt(2))},
|
||||||
|
Depth: 1,
|
||||||
|
ReturnData: []byte{0x12, 0x34},
|
||||||
|
},
|
||||||
|
want: `{"pc":7,"op":"SSTORE","gas":100,"gasCost":20,"depth":1,"returnData":"0x1234","memory":["0xaabb000000000000000000000000000000000000000000000000000000000000"],"storage":{"0x0000000000000000000000000000000000000000000000000000000000000001":"0x0000000000000000000000000000000000000000000000000000000000000002"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "includes error only when present",
|
||||||
|
log: &StructLog{
|
||||||
|
Pc: 1,
|
||||||
|
Op: vm.STOP,
|
||||||
|
Gas: 2,
|
||||||
|
GasCost: 3,
|
||||||
|
Depth: 1,
|
||||||
|
Err: errors.New("boom"),
|
||||||
|
},
|
||||||
|
want: `{"pc":1,"op":"STOP","gas":2,"gasCost":3,"depth":1,"error":"boom"}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
have := string(tt.log.toLegacyJSON())
|
||||||
|
if have != tt.want {
|
||||||
|
t.Fatalf("mismatched results\n\thave: %v\n\twant: %v", have, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue