Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions x/vm/types/trace_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package types

import "encoding/json"

// traceConfigJSON is an auxiliary struct for custom JSON unmarshaling.
// The key difference is TracerConfig uses json.RawMessage to accept both
// JSON objects and strings, enabling Ethereum JSON-RPC standard compliance.
type traceConfigJSON struct {
Tracer string `json:"tracer,omitempty"`
Timeout string `json:"timeout,omitempty"`
Reexec uint64 `json:"reexec,omitempty"`
DisableStack bool `json:"disableStack"`
DisableStorage bool `json:"disableStorage"`
Debug bool `json:"debug,omitempty"`
Limit int32 `json:"limit,omitempty"`
Overrides *ChainConfig `json:"overrides,omitempty"`
EnableMemory bool `json:"enableMemory"`
EnableReturnData bool `json:"enableReturnData"`
TracerConfig json.RawMessage `json:"tracerConfig,omitempty"`
}

// UnmarshalJSON implements custom JSON unmarshaling for TraceConfig.
// This enables the tracerConfig field to accept both:
// - JSON objects (Ethereum standard): {"tracerConfig": {"onlyTopCall": true}}
// - Escaped JSON strings (legacy): {"tracerConfig": "{\"onlyTopCall\": true}"}
//
// The Ethereum JSON-RPC standard expects tracerConfig as a JSON object,
// but the protobuf-generated struct has it as a string field. This custom
// unmarshaler bridges that gap by accepting json.RawMessage and storing
// it as a string in TracerJsonConfig.
func (tc *TraceConfig) UnmarshalJSON(data []byte) error {
var aux traceConfigJSON
if err := json.Unmarshal(data, &aux); err != nil {
return err
}

tc.Tracer = aux.Tracer
tc.Timeout = aux.Timeout
tc.Reexec = aux.Reexec
tc.DisableStack = aux.DisableStack
tc.DisableStorage = aux.DisableStorage
tc.Debug = aux.Debug
tc.Limit = aux.Limit
tc.Overrides = aux.Overrides
tc.EnableMemory = aux.EnableMemory
tc.EnableReturnData = aux.EnableReturnData

if len(aux.TracerConfig) > 0 {
tc.TracerJsonConfig = string(aux.TracerConfig)
}

return nil
}

// MarshalJSON implements custom JSON marshaling for TraceConfig.
// Outputs tracerConfig as raw JSON (not an escaped string) for
// Ethereum JSON-RPC standard compliance.
func (tc TraceConfig) MarshalJSON() ([]byte, error) {
aux := struct {
Tracer string `json:"tracer,omitempty"`
Timeout string `json:"timeout,omitempty"`
Reexec uint64 `json:"reexec,omitempty"`
DisableStack bool `json:"disableStack"`
DisableStorage bool `json:"disableStorage"`
Debug bool `json:"debug,omitempty"`
Limit int32 `json:"limit,omitempty"`
Overrides *ChainConfig `json:"overrides,omitempty"`
EnableMemory bool `json:"enableMemory"`
EnableReturnData bool `json:"enableReturnData"`
TracerConfig json.RawMessage `json:"tracerConfig,omitempty"`
}{
Tracer: tc.Tracer,
Timeout: tc.Timeout,
Reexec: tc.Reexec,
DisableStack: tc.DisableStack,
DisableStorage: tc.DisableStorage,
Debug: tc.Debug,
Limit: tc.Limit,
Overrides: tc.Overrides,
EnableMemory: tc.EnableMemory,
EnableReturnData: tc.EnableReturnData,
}

if tc.TracerJsonConfig != "" {
aux.TracerConfig = json.RawMessage(tc.TracerJsonConfig)
}

return json.Marshal(aux)
}
213 changes: 213 additions & 0 deletions x/vm/types/trace_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package types_test

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"

evmtypes "github.com/cosmos/evm/x/vm/types"
)

func TestTraceConfig_UnmarshalJSON_ObjectFormat(t *testing.T) {
// Ethereum standard format - tracerConfig as JSON object
input := `{"tracer":"callTracer","tracerConfig":{"onlyTopCall":true}}`

var tc evmtypes.TraceConfig
err := json.Unmarshal([]byte(input), &tc)
require.NoError(t, err)
require.Equal(t, "callTracer", tc.Tracer)
require.Equal(t, `{"onlyTopCall":true}`, tc.TracerJsonConfig)
}

func TestTraceConfig_UnmarshalJSON_ComplexObjectFormat(t *testing.T) {
// More complex tracerConfig object
input := `{"tracer":"callTracer","tracerConfig":{"onlyTopCall":true,"withLog":false,"diffMode":true}}`

var tc evmtypes.TraceConfig
err := json.Unmarshal([]byte(input), &tc)
require.NoError(t, err)
require.Equal(t, "callTracer", tc.Tracer)
require.Equal(t, `{"onlyTopCall":true,"withLog":false,"diffMode":true}`, tc.TracerJsonConfig)
}

func TestTraceConfig_UnmarshalJSON_StringFormat(t *testing.T) {
// Legacy format - tracerConfig as escaped string (backwards compatibility)
input := `{"tracer":"callTracer","tracerConfig":"{\"onlyTopCall\":true}"}`

var tc evmtypes.TraceConfig
err := json.Unmarshal([]byte(input), &tc)
require.NoError(t, err)
require.Equal(t, "callTracer", tc.Tracer)
// When input is a string, json.RawMessage preserves it as a quoted string
require.Equal(t, `"{\"onlyTopCall\":true}"`, tc.TracerJsonConfig)
}

func TestTraceConfig_UnmarshalJSON_NoTracerConfig(t *testing.T) {
input := `{"tracer":"callTracer","disableStack":true}`

var tc evmtypes.TraceConfig
err := json.Unmarshal([]byte(input), &tc)
require.NoError(t, err)
require.Equal(t, "callTracer", tc.Tracer)
require.True(t, tc.DisableStack)
require.Empty(t, tc.TracerJsonConfig)
}

func TestTraceConfig_UnmarshalJSON_AllFields(t *testing.T) {
input := `{
"tracer": "callTracer",
"timeout": "10s",
"reexec": 128,
"disableStack": true,
"disableStorage": true,
"debug": true,
"limit": 1000,
"enableMemory": true,
"enableReturnData": true,
"tracerConfig": {"onlyTopCall": true}
}`

var tc evmtypes.TraceConfig
err := json.Unmarshal([]byte(input), &tc)
require.NoError(t, err)
require.Equal(t, "callTracer", tc.Tracer)
require.Equal(t, "10s", tc.Timeout)
require.Equal(t, uint64(128), tc.Reexec)
require.True(t, tc.DisableStack)
require.True(t, tc.DisableStorage)
require.True(t, tc.Debug)
require.Equal(t, int32(1000), tc.Limit)
require.True(t, tc.EnableMemory)
require.True(t, tc.EnableReturnData)
require.Equal(t, `{"onlyTopCall": true}`, tc.TracerJsonConfig)
}

func TestTraceConfig_UnmarshalJSON_EmptyObject(t *testing.T) {
input := `{}`

var tc evmtypes.TraceConfig
err := json.Unmarshal([]byte(input), &tc)
require.NoError(t, err)
require.Empty(t, tc.Tracer)
require.Empty(t, tc.TracerJsonConfig)
}

func TestTraceConfig_UnmarshalJSON_NullTracerConfig(t *testing.T) {
input := `{"tracer":"callTracer","tracerConfig":null}`

var tc evmtypes.TraceConfig
err := json.Unmarshal([]byte(input), &tc)
require.NoError(t, err)
require.Equal(t, "callTracer", tc.Tracer)
// null is preserved as "null" in json.RawMessage
require.Equal(t, "null", tc.TracerJsonConfig)
}

func TestTraceConfig_MarshalJSON(t *testing.T) {
tc := evmtypes.TraceConfig{
Tracer: "callTracer",
TracerJsonConfig: `{"onlyTopCall":true}`,
}

data, err := json.Marshal(tc)
require.NoError(t, err)

// Verify it outputs as raw JSON object, not escaped string
require.Contains(t, string(data), `"tracerConfig":{"onlyTopCall":true}`)
require.NotContains(t, string(data), `"tracerConfig":"{`)
}

func TestTraceConfig_MarshalJSON_NoTracerConfig(t *testing.T) {
tc := evmtypes.TraceConfig{
Tracer: "callTracer",
}

data, err := json.Marshal(tc)
require.NoError(t, err)

// tracerConfig should be omitted when empty
require.NotContains(t, string(data), `"tracerConfig"`)
}

func TestTraceConfig_MarshalJSON_AllFields(t *testing.T) {
tc := evmtypes.TraceConfig{
Tracer: "callTracer",
Timeout: "10s",
Reexec: 128,
DisableStack: true,
DisableStorage: true,
Debug: true,
Limit: 1000,
EnableMemory: true,
EnableReturnData: true,
TracerJsonConfig: `{"onlyTopCall":true}`,
}

data, err := json.Marshal(tc)
require.NoError(t, err)

// Unmarshal to verify structure
var result map[string]interface{}
err = json.Unmarshal(data, &result)
require.NoError(t, err)

require.Equal(t, "callTracer", result["tracer"])
require.Equal(t, "10s", result["timeout"])
require.Equal(t, float64(128), result["reexec"])
require.Equal(t, true, result["disableStack"])
require.Equal(t, true, result["disableStorage"])
require.Equal(t, true, result["debug"])
require.Equal(t, float64(1000), result["limit"])
require.Equal(t, true, result["enableMemory"])
require.Equal(t, true, result["enableReturnData"])

// tracerConfig should be a map, not a string
tracerConfig, ok := result["tracerConfig"].(map[string]interface{})
require.True(t, ok, "tracerConfig should be an object, not a string")
require.Equal(t, true, tracerConfig["onlyTopCall"])
}

func TestTraceConfig_RoundTrip(t *testing.T) {
original := evmtypes.TraceConfig{
Tracer: "callTracer",
Timeout: "5s",
DisableStack: true,
EnableMemory: true,
TracerJsonConfig: `{"onlyTopCall":true,"withLog":false}`,
}

data, err := json.Marshal(original)
require.NoError(t, err)

var restored evmtypes.TraceConfig
err = json.Unmarshal(data, &restored)
require.NoError(t, err)

require.Equal(t, original.Tracer, restored.Tracer)
require.Equal(t, original.Timeout, restored.Timeout)
require.Equal(t, original.DisableStack, restored.DisableStack)
require.Equal(t, original.EnableMemory, restored.EnableMemory)
require.Equal(t, original.TracerJsonConfig, restored.TracerJsonConfig)
}

func TestTraceConfig_RoundTrip_FromObjectInput(t *testing.T) {
// Start with JSON object format (Ethereum standard)
input := `{"tracer":"callTracer","tracerConfig":{"onlyTopCall":true,"withLog":false}}`

var tc evmtypes.TraceConfig
err := json.Unmarshal([]byte(input), &tc)
require.NoError(t, err)

// Marshal back
data, err := json.Marshal(tc)
require.NoError(t, err)

// Unmarshal again
var restored evmtypes.TraceConfig
err = json.Unmarshal(data, &restored)
require.NoError(t, err)

require.Equal(t, tc.Tracer, restored.Tracer)
require.Equal(t, tc.TracerJsonConfig, restored.TracerJsonConfig)
}