Skip to content

Commit 8e3e476

Browse files
Merge pull request #85 from HorizenOfficial/as/bigint_json_representation
As/bigint json representation
2 parents 7fd639e + 05e5302 commit 8e3e476

26 files changed

Lines changed: 1109 additions & 269 deletions

app/simple/app/app_test.go

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -686,11 +686,11 @@ func TestBigIntUint256JSONRoundTrip(t *testing.T) {
686686
orig := new(big.Int)
687687
orig.SetString("115792089237316195423570985008687907853269984665640564039457584007913129639935", 10) // 2^256-1
688688

689-
// Step 2: marshal big.Int into JSON
689+
// Step 2: marshal *common.Big into JSON
690690
type HostStruct struct {
691-
Amount *big.Int `json:"amount"`
691+
Amount *common.Big `json:"amount"`
692692
}
693-
hostObj := HostStruct{Amount: orig}
693+
hostObj := HostStruct{Amount: common.ToBig(orig)}
694694

695695
jsonData, err := json.Marshal(hostObj)
696696
if err != nil {
@@ -721,9 +721,130 @@ func TestBigIntUint256JSONRoundTrip(t *testing.T) {
721721
}
722722

723723
// Step 6: compare
724-
if orig.Cmp(hostObj2.Amount) != 0 {
724+
if orig.Cmp(hostObj2.Amount.ToInt()) != 0 {
725725
t.Errorf("round-trip mismatch:\noriginal: %s\nfinal: %s", orig.String(), hostObj2.Amount.String())
726726
} else {
727727
t.Logf("Round-trip successful: value preserved exactly")
728728
}
729729
}
730+
731+
// TestUint256BigJSONCompatibility verifies that Uint256 and common.Big produce
732+
// identical JSON representations and can be unmarshaled interchangeably.
733+
func TestUint256BigJSONCompatibility(t *testing.T) {
734+
// Test values covering edge cases
735+
testValues := []struct {
736+
name string
737+
decimal string
738+
hex string
739+
}{
740+
{"zero", "0", "0x0"},
741+
{"one", "1", "0x1"},
742+
{"small", "255", "0xff"},
743+
{"medium", "12345", "0x3039"},
744+
{"large", "12345678901234567890", "0xab54a98ceb1f0ad2"},
745+
{"max_uint64", "18446744073709551615", "0xffffffffffffffff"},
746+
{"max_uint128", "340282366920938463463374607431768211455", "0xffffffffffffffffffffffffffffffff"},
747+
{"max_uint256", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"},
748+
}
749+
750+
for _, tt := range testValues {
751+
t.Run(tt.name, func(t *testing.T) {
752+
// Create big.Int from decimal
753+
bi, ok := new(big.Int).SetString(tt.decimal, 10)
754+
require.True(t, ok, "Failed to parse decimal: %s", tt.decimal)
755+
756+
// Create common.Big and Uint256 from the same value
757+
bigVal := common.ToBig(bi)
758+
uint256Val := new(Uint256).SetBytes(bi.Bytes())
759+
760+
// Marshal both to JSON
761+
bigJSON, err := json.Marshal(bigVal)
762+
require.NoError(t, err, "Failed to marshal common.Big")
763+
764+
uint256JSON, err := json.Marshal(uint256Val)
765+
require.NoError(t, err, "Failed to marshal Uint256")
766+
767+
// Verify JSON is byte-for-byte identical
768+
require.Equal(t, string(bigJSON), string(uint256JSON),
769+
"JSON mismatch for %s:\n common.Big: %s\n Uint256: %s",
770+
tt.name, string(bigJSON), string(uint256JSON))
771+
772+
// Verify JSON matches expected hex format
773+
expectedJSON := `"` + tt.hex + `"`
774+
require.Equal(t, expectedJSON, string(bigJSON),
775+
"Unexpected JSON format for %s", tt.name)
776+
777+
// Test cross-type unmarshal: Big JSON → Uint256
778+
var u256FromBig Uint256
779+
err = json.Unmarshal(bigJSON, &u256FromBig)
780+
require.NoError(t, err, "Failed to unmarshal Big JSON into Uint256")
781+
require.Equal(t, tt.decimal, u256FromBig.String(),
782+
"Value mismatch after Big→Uint256 unmarshal")
783+
784+
// Test cross-type unmarshal: Uint256 JSON → Big
785+
var bigFromU256 common.Big
786+
err = json.Unmarshal(uint256JSON, &bigFromU256)
787+
require.NoError(t, err, "Failed to unmarshal Uint256 JSON into Big")
788+
require.Equal(t, tt.decimal, bigFromU256.String(),
789+
"Value mismatch after Uint256→Big unmarshal")
790+
})
791+
}
792+
}
793+
794+
// TestUint256BigStructCompatibility verifies that structs containing Uint256
795+
// and common.Big fields produce compatible JSON.
796+
func TestUint256BigStructCompatibility(t *testing.T) {
797+
// Simulate host-side struct (uses common.Big)
798+
type HostStruct struct {
799+
Amount *common.Big `json:"amount"`
800+
Fee *common.Big `json:"fee"`
801+
Balance *common.Big `json:"balance"`
802+
}
803+
804+
// Simulate WASM-side struct (uses Uint256)
805+
type WASMStruct struct {
806+
Amount Uint256 `json:"amount"`
807+
Fee Uint256 `json:"fee"`
808+
Balance Uint256 `json:"balance"`
809+
}
810+
811+
// Create host struct with test values
812+
hostStruct := HostStruct{
813+
Amount: common.NewBig(1000000),
814+
Fee: common.NewBig(100),
815+
Balance: common.NewBig(999900),
816+
}
817+
818+
// Marshal host struct
819+
hostJSON, err := json.Marshal(hostStruct)
820+
require.NoError(t, err)
821+
t.Logf("Host JSON: %s", string(hostJSON))
822+
823+
// Unmarshal into WASM struct
824+
var wasmStruct WASMStruct
825+
err = json.Unmarshal(hostJSON, &wasmStruct)
826+
require.NoError(t, err)
827+
828+
// Verify values match
829+
require.Equal(t, "1000000", wasmStruct.Amount.String())
830+
require.Equal(t, "100", wasmStruct.Fee.String())
831+
require.Equal(t, "999900", wasmStruct.Balance.String())
832+
833+
// Marshal WASM struct back
834+
wasmJSON, err := json.Marshal(wasmStruct)
835+
require.NoError(t, err)
836+
t.Logf("WASM JSON: %s", string(wasmJSON))
837+
838+
// JSON should be identical
839+
require.JSONEq(t, string(hostJSON), string(wasmJSON))
840+
841+
// Unmarshal back into host struct
842+
var hostStruct2 HostStruct
843+
err = json.Unmarshal(wasmJSON, &hostStruct2)
844+
require.NoError(t, err)
845+
846+
// Final values should match original
847+
require.Equal(t, 0, hostStruct.Amount.ToInt().Cmp(hostStruct2.Amount.ToInt()))
848+
require.Equal(t, 0, hostStruct.Fee.ToInt().Cmp(hostStruct2.Fee.ToInt()))
849+
require.Equal(t, 0, hostStruct.Balance.ToInt().Cmp(hostStruct2.Balance.ToInt()))
850+
}

app/simple/app/uint256.go

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ package app
22

33
import (
44
"encoding/binary"
5-
"encoding/json"
65
"errors"
76
"fmt"
87
"math/bits"
9-
"strings"
108
)
119

1210
// Uint256 represents a 256-bit unsigned integer using 4 uint64 values.
@@ -137,52 +135,82 @@ func (z Uint256) divModWord(divisor uint64) (Uint256, uint64) {
137135
return quot, r
138136
}
139137

138+
// ToHex returns the hex representation of z with "0x" prefix.
139+
func (z Uint256) ToHex() string {
140+
if z.IsZero() {
141+
return "0x0"
142+
}
143+
// Similar logic to String() but base 16
144+
val := z
145+
res := make([]byte, 0, 66) // 0x + 64 hex digits
146+
147+
const sixteen = uint64(16)
148+
const hexChars = "0123456789abcdef"
149+
150+
for !val.IsZero() {
151+
var rem uint64
152+
val, rem = val.divModWord(sixteen)
153+
res = append(res, hexChars[rem])
154+
}
155+
res = append(res, 'x', '0')
156+
157+
// reverse
158+
for i, j := 0, len(res)-1; i < j; i, j = i+1, j-1 {
159+
res[i], res[j] = res[j], res[i]
160+
}
161+
return string(res)
162+
}
163+
140164
// MarshalJSON implements json.Marshaler.
141-
// It marshals the Uint256 as a JSON number (no quotes)
142-
// math/big.Int marshals as digits.
165+
// It marshals the Uint256 as a hex string with 0x prefix.
143166
func (z Uint256) MarshalJSON() ([]byte, error) {
144-
return []byte(z.String()), nil
167+
s := z.ToHex()
168+
buf := make([]byte, 0, len(s)+2)
169+
buf = append(buf, '"')
170+
buf = append(buf, s...)
171+
buf = append(buf, '"')
172+
return buf, nil
145173
}
146174

147175
// UnmarshalJSON implements json.Unmarshaler.
148-
// Only decimal strings or numbers are accepted. Overflow returns an error.
176+
// Only hex strings with "0x" prefix are accepted.
149177
func (z *Uint256) UnmarshalJSON(data []byte) error {
150-
var s string
151-
152-
// 1. Determine if input is a JSON string or raw number by peeking the first non-whitespace char using a recursive for loop
153-
trimmedData := data
154-
for len(trimmedData) > 0 && (trimmedData[0] == ' ' || trimmedData[0] == '\t' || trimmedData[0] == '\n' || trimmedData[0] == '\r') {
155-
trimmedData = trimmedData[1:]
178+
if string(data) == "null" {
179+
*z = Uint256{}
180+
return nil
156181
}
182+
if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
183+
return fmt.Errorf("invalid Uint256 format: %s", string(data))
184+
}
185+
s := string(data[1 : len(data)-1])
157186

158-
if len(trimmedData) > 0 && trimmedData[0] == '"' {
159-
// First non-whitespace character is a quote, this is a JSON string
160-
if err := json.Unmarshal(data, &s); err != nil {
161-
return err
162-
}
163-
} else {
164-
// First non-whitespace character is not a quote, this is a raw JSON number or we have empty input
165-
s = string(trimmedData)
187+
if len(s) < 2 || s[0] != '0' || s[1] != 'x' {
188+
return fmt.Errorf("invalid Uint256 prefix: %s (only lowercase 0x is accepted)", s)
166189
}
167190

168-
// 2. Clean up surrounding whitespace and validate non-empty
169-
s = strings.TrimSpace(s)
191+
*z = Uint256{}
192+
s = s[2:]
170193
if len(s) == 0 {
171-
return errors.New("Uint256 value is empty")
194+
return fmt.Errorf("invalid Uint256 format: empty hex string after 0x prefix")
172195
}
173196

174-
// 3. Strict digit-only parsing
175-
*z = Uint256{}
176-
const ten = uint64(10)
197+
const sixteen = uint64(16)
177198
for _, c := range s {
178-
if c < '0' || c > '9' {
179-
return fmt.Errorf("invalid character in Uint256: %c", c)
199+
var digit uint64
200+
switch {
201+
case c >= '0' && c <= '9':
202+
digit = uint64(c - '0')
203+
case c >= 'a' && c <= 'f':
204+
digit = uint64(c - 'a' + 10)
205+
case c >= 'A' && c <= 'F':
206+
digit = uint64(c - 'A' + 10)
207+
default:
208+
return fmt.Errorf("invalid hex character in Uint256: %c", c)
180209
}
181-
digit := uint64(c - '0')
182210

183-
if z.Mul64Overflow(ten) {
211+
if z.Mul64Overflow(sixteen) {
184212
// we have modified z actually, but caller must check the error
185-
return errors.New("Uint256 overflow after multiplication")
213+
return errors.New("Uint256 overflow after mul")
186214
}
187215
if z.Add64Overflow(digit) {
188216
// we have modified z actually, but caller must check the error

0 commit comments

Comments
 (0)