Skip to content

Commit b69b825

Browse files
authored
fix(rpc): felt types serialize as u64 limbs instead of hex strings (#3504)
1 parent 422733a commit b69b825

5 files changed

Lines changed: 99 additions & 17 deletions

File tree

core/felt/address.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ func (a *Address) UnmarshalJSON(data []byte) error {
1414
return (*Felt)(a).UnmarshalJSON(data)
1515
}
1616

17-
func (a *Address) MarshalJSON() ([]byte, error) {
18-
return (*Felt)(a).MarshalJSON()
17+
func (a Address) MarshalJSON() ([]byte, error) {
18+
return Felt(a).MarshalJSON()
1919
}
2020

2121
func (a *Address) Marshal() []byte {

core/felt/felt.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@ func (z *Felt) UnmarshalJSON(data []byte) error {
6868
return err
6969
}
7070

71-
// MarshalJSON forwards the call to underlying field element implementation
72-
func (z *Felt) MarshalJSON() ([]byte, error) {
71+
// MarshalJSON forwards the call to underlying field element implementation.
72+
// Uses a value receiver so encoding/json can call it on non-addressable values
73+
// (e.g. struct fields inside a value stored in an `any` interface).
74+
func (z Felt) MarshalJSON() ([]byte, error) {
7375
return []byte("\"" + z.String() + "\""), nil
7476
}
7577

core/felt/felt_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package felt_test
22

33
import (
4+
"encoding/json"
5+
"strings"
46
"testing"
57

68
"github.com/NethermindEth/juno/core/felt"
@@ -63,6 +65,80 @@ func TestShortString(t *testing.T) {
6365
})
6466
}
6567

68+
// TestMarshalJSON_ValueInInterface reproduces a bug where felt types serialised
69+
// as [4]uint64 limbs instead of hex strings. This happens because MarshalJSON
70+
// uses a pointer receiver, but encoding/json cannot call pointer-receiver methods
71+
// on non-addressable values (e.g. struct fields inside a value stored in `any`).
72+
func TestMarshalJSON_ValueInInterface(t *testing.T) {
73+
f := felt.UnsafeFromString[felt.Felt]("0xdeadbeef")
74+
75+
// Simulates how jsonrpc server stores handler results:
76+
// response.Result = handlerReturnValue (stored as value in `any`)
77+
type rpcResponse struct {
78+
Result any `json:"result"`
79+
}
80+
81+
tests := []struct {
82+
name string
83+
input any
84+
}{
85+
{
86+
name: "Felt value field in struct value",
87+
input: struct {
88+
V felt.Felt `json:"v"`
89+
}{V: f},
90+
},
91+
{
92+
name: "TransactionHash value field in struct value",
93+
input: struct {
94+
V felt.TransactionHash `json:"v"`
95+
}{V: felt.TransactionHash(f)},
96+
},
97+
{
98+
name: "Hash value field in struct value",
99+
input: struct {
100+
V felt.Hash `json:"v"`
101+
}{V: felt.Hash(f)},
102+
},
103+
{
104+
name: "Address value field in struct value",
105+
input: struct {
106+
V felt.Address `json:"v"`
107+
}{V: felt.Address(f)},
108+
},
109+
{
110+
name: "ClassHash value field in struct value",
111+
input: struct {
112+
V felt.ClassHash `json:"v"`
113+
}{V: felt.ClassHash(f)},
114+
},
115+
{
116+
name: "pointer field works (control)",
117+
input: struct {
118+
V *felt.Felt `json:"v"`
119+
}{V: &f},
120+
},
121+
}
122+
123+
for _, tt := range tests {
124+
t.Run(tt.name, func(t *testing.T) {
125+
resp := rpcResponse{Result: tt.input}
126+
b, err := json.Marshal(resp)
127+
require.NoError(t, err)
128+
129+
s := string(b)
130+
t.Logf("JSON output: %s", s)
131+
132+
// Must contain "0xdeadbeef" as a hex string, not uint64 limbs
133+
assert.Contains(t, s, "0xdeadbeef",
134+
"expected hex string, got limbs — pointer-receiver MarshalJSON"+
135+
" not called on non-addressable value")
136+
assert.False(t, strings.Contains(s, "[") && strings.Contains(s, ","),
137+
"got JSON array (raw [4]uint64 limbs) instead of hex string")
138+
})
139+
}
140+
}
141+
66142
func TestFeltMarshalAndUnmarshal(t *testing.T) {
67143
f := new(felt.Felt).SetBytes([]byte("somebytes"))
68144

core/felt/hash.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ func (h *Hash) UnmarshalJSON(data []byte) error {
1414
return (*Felt)(h).UnmarshalJSON(data)
1515
}
1616

17-
func (h *Hash) MarshalJSON() ([]byte, error) {
18-
return (*Felt)(h).MarshalJSON()
17+
func (h Hash) MarshalJSON() ([]byte, error) {
18+
return Felt(h).MarshalJSON()
1919
}
2020

2121
func (h *Hash) Marshal() []byte {
@@ -40,8 +40,8 @@ func (h *ClassHash) UnmarshalJSON(data []byte) error {
4040
return (*Hash)(h).UnmarshalJSON(data)
4141
}
4242

43-
func (h *ClassHash) MarshalJSON() ([]byte, error) {
44-
return (*Hash)(h).MarshalJSON()
43+
func (h ClassHash) MarshalJSON() ([]byte, error) {
44+
return Hash(h).MarshalJSON()
4545
}
4646

4747
func (h *ClassHash) Marshal() []byte {
@@ -66,8 +66,8 @@ func (h *SierraClassHash) UnmarshalJSON(data []byte) error {
6666
return (*ClassHash)(h).UnmarshalJSON(data)
6767
}
6868

69-
func (h *SierraClassHash) MarshalJSON() ([]byte, error) {
70-
return (*ClassHash)(h).MarshalJSON()
69+
func (h SierraClassHash) MarshalJSON() ([]byte, error) {
70+
return ClassHash(h).MarshalJSON()
7171
}
7272

7373
func (h *SierraClassHash) Marshal() []byte {
@@ -92,8 +92,8 @@ func (h *CasmClassHash) UnmarshalJSON(data []byte) error {
9292
return (*ClassHash)(h).UnmarshalJSON(data)
9393
}
9494

95-
func (h *CasmClassHash) MarshalJSON() ([]byte, error) {
96-
return (*ClassHash)(h).MarshalJSON()
95+
func (h CasmClassHash) MarshalJSON() ([]byte, error) {
96+
return ClassHash(h).MarshalJSON()
9797
}
9898

9999
func (h *CasmClassHash) Marshal() []byte {
@@ -118,8 +118,8 @@ func (h *TransactionHash) UnmarshalJSON(data []byte) error {
118118
return (*Hash)(h).UnmarshalJSON(data)
119119
}
120120

121-
func (h *TransactionHash) MarshalJSON() ([]byte, error) {
122-
return (*Hash)(h).MarshalJSON()
121+
func (h TransactionHash) MarshalJSON() ([]byte, error) {
122+
return Hash(h).MarshalJSON()
123123
}
124124

125125
func (h *TransactionHash) Marshal() []byte {
@@ -144,8 +144,8 @@ func (h *StateRootHash) UnmarshalJSON(data []byte) error {
144144
return (*Hash)(h).UnmarshalJSON(data)
145145
}
146146

147-
func (h *StateRootHash) MarshalJSON() ([]byte, error) {
148-
return (*Hash)(h).MarshalJSON()
147+
func (h StateRootHash) MarshalJSON() ([]byte, error) {
148+
return Hash(h).MarshalJSON()
149149
}
150150

151151
func (h *StateRootHash) Marshal() []byte {

rpc/v6/trace_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,11 @@ func TestTransactionTraceValidation(t *testing.T) {
218218
func TestFunctionInvocationMarshalling(t *testing.T) {
219219
t.Run("All FunctionInvocation fields must get marshalled", func(t *testing.T) {
220220
zeroValuedFnInvocation := rpc.FunctionInvocation{}
221-
expected := `{"contract_address":[0,0,0,0],"entry_point_selector":null,"calldata":null,"caller_address":[0,0,0,0],"class_hash":null,"entry_point_type":"","call_type":"","result":null,"calls":null,"events":null,"messages":null,"execution_resources":null}`
221+
expected := `{"contract_address":"0x0","entry_point_selector":null,` +
222+
`"calldata":null,"caller_address":"0x0","class_hash":null,` +
223+
`"entry_point_type":"","call_type":"","result":null,` +
224+
`"calls":null,"events":null,"messages":null,` +
225+
`"execution_resources":null}`
222226

223227
jsonStr, err := json.Marshal(zeroValuedFnInvocation)
224228

0 commit comments

Comments
 (0)