Skip to content

Commit

Permalink
conversion: behavioural changes in String, MarshalText and `Marsh…
Browse files Browse the repository at this point in the history
…alJSON` (#144)

This PR changes the way marshalling and unmarshalling behaves, in order to maximize compatibilty with `big.Int`: as in, maximize the chance that a 'drop in' replacement of `big.Int` for `uint256.Int` will work seamlessly. 

- `String()` - before this change, `String()` would return the integer in hexadecimal format. This PR changes it to instead return the input in decimal format, like `big.Int` does. 
- `MarshalText()` now returns the integer in decimal format, previously hexadecimal. 
- `MarshalJSON()` now returns the integer in decimal format, previously hexadecimal. 
- `UnmarshalText` now accepts either hex, `0x234` or `234`. Previously it accepted _only_ hex.
- `UnmarshalJSON` now accepts either hex-string, `"0x234"`, dec-string `"234"` or naked numeric decimal  `234`. Previously it accepted _only_ string-hex.

 JSON marshalling is, alas, not 100% compatible, since `big.Int` marshals to json numeric format: `{ Foo: 5}` as opposed to string-format: `{ Foo: "5" }`. The former is not ideal for large numbers, since platforms like javascript do not support arbitary large numbers, usually capped at `53` bits or so. 


TLDR; with this change, some output-formats change from Hex to Dec, but also some input-formats become more accepting. This change achieves better -- but not total -- compatibility with big.Int marshalling.
  • Loading branch information
holiman authored Nov 27, 2023
1 parent b4f79ca commit f24ed59
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 28 deletions.
37 changes: 25 additions & 12 deletions conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func FromBig(b *big.Int) (*Int, bool) {

// MustFromBig is a convenience-constructor from big.Int.
// Returns a new Int and panics if overflow occurred.
// OBS: If b is `nil`, this method does _not_ panic, but
// OBS: If b is `nil`, this method does _not_ panic, but
// instead returns `nil`
func MustFromBig(b *big.Int) *Int {
if b == nil {
Expand Down Expand Up @@ -135,7 +135,6 @@ func (z *Int) Float64() float64 {
// - This method does not accept negative zero as valid, e.g "-0x0",
// - (this method does not accept any negative input as valid)
func (z *Int) SetFromHex(hex string) error {
z.Clear()
return z.fromHex(hex)
}

Expand All @@ -147,6 +146,7 @@ func (z *Int) fromHex(hex string) error {
if len(hex) > 66 {
return ErrBig256Range
}
z.Clear()
end := len(hex)
for i := 0; i < 4; i++ {
start := end - 16
Expand Down Expand Up @@ -188,10 +188,14 @@ func MustFromHex(hex string) *Int {
return &z
}

// UnmarshalText implements encoding.TextUnmarshaler
// UnmarshalText implements encoding.TextUnmarshaler. This method
// can unmarshal either hexadecimal or decimal.
// - For hexadecimal, the input _must_ be prefixed with 0x or 0X
func (z *Int) UnmarshalText(input []byte) error {
z.Clear()
return z.fromHex(string(input))
if len(input) >= 2 && input[0] == '0' && (input[1] == 'x' || input[1] == 'X') {
return z.fromHex(string(input))
}
return z.fromDecimal(string(input))
}

// SetFromBig converts a big.Int to Int and sets the value to z.
Expand Down Expand Up @@ -613,26 +617,36 @@ func (z *Int) EncodeRLP(w io.Writer) error {
}

// MarshalText implements encoding.TextMarshaler
// MarshalText marshals using the decimal representation (compatible with big.Int)
func (z *Int) MarshalText() ([]byte, error) {
return []byte(z.Hex()), nil
return []byte(z.Dec()), nil
}

// MarshalJSON implements json.Marshaler.
// MarshalJSON marshals using the 'decimal string' representation. This is _not_ compatible
// with big.Int: big.Int marshals into JSON 'native' numeric format.
//
// The JSON native format is, on some platforms, (e.g. javascript), limited to 53-bit large
// integer space. Thus, U256 uses string-format, which is not compatible with
// big.int (big.Int refuses to unmarshal a string representation).
func (z *Int) MarshalJSON() ([]byte, error) {
return []byte(`"` + z.Hex() + `"`), nil
return []byte(`"` + z.Dec() + `"`), nil
}

// UnmarshalJSON implements json.Unmarshaler.
// UnmarshalJSON implements json.Unmarshaler. UnmarshalJSON accepts either
// - Quoted string: either hexadecimal OR decimal
// - Not quoted string: only decimal
func (z *Int) UnmarshalJSON(input []byte) error {
if len(input) < 2 || input[0] != '"' || input[len(input)-1] != '"' {
return ErrNonString
// if not quoted, it must be decimal
return z.fromDecimal(string(input))
}
return z.UnmarshalText(input[1 : len(input)-1])
}

// String returns the hex encoding of b.
// String returns the decimal encoding of b.
func (z *Int) String() string {
return z.Hex()
return z.Dec()
}

const (
Expand Down Expand Up @@ -738,7 +752,6 @@ var (
ErrEmptyNumber = errors.New("hex string \"0x\"")
ErrLeadingZero = errors.New("hex number with leading zero digits")
ErrBig256Range = errors.New("hex number > 256 bits")
ErrNonString = errors.New("non-string")
ErrBadBufferLength = errors.New("bad ssz buffer length")
ErrBadEncodedLength = errors.New("bad ssz encoded length")
)
Expand Down
57 changes: 41 additions & 16 deletions conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1196,10 +1196,12 @@ func TestDecode(t *testing.T) {
Foo *Int
}
var jsonDecoded jsonStruct
if err := json.Unmarshal([]byte(`{"Foo":0x1}`), &jsonDecoded); err == nil {
t.Fatal("Expected error")
// This test was previously an "expected error", The U256 behaviour has now
// changed, to be compatible with big.Int
if err := json.Unmarshal([]byte(`{"Foo":1}`), &jsonDecoded); err != nil {
t.Fatalf("Expected no error, have %v", err)
}
if err := json.Unmarshal([]byte(`{"Foo":1}`), &jsonDecoded); err == nil {
if err := json.Unmarshal([]byte(`{"Foo":0x1}`), &jsonDecoded); err == nil {
t.Fatal("Expected error")
}
if err := json.Unmarshal([]byte(`{"Foo":""}`), &jsonDecoded); err == nil {
Expand All @@ -1216,38 +1218,61 @@ func TestEnDecode(t *testing.T) {
type jsonStruct struct {
Foo *Int
}
type jsonBigStruct struct {
Foo *big.Int
}
var testSample = func(i int, bigSample big.Int, intSample Int) {
// Encoding
wantHex := fmt.Sprintf("0x%s", bigSample.Text(16))
wantDec := bigSample.Text(10)

if got := intSample.Hex(); wantHex != got {
t.Fatalf("test %d #1, got %v, exp %v", i, got, wantHex)
if have, want := intSample.Hex(), fmt.Sprintf("0x%s", bigSample.Text(16)); have != want {
t.Fatalf("test %d #1, have %v, want %v", i, have, want)
}
if got := intSample.String(); wantHex != got {
t.Fatalf("test %d #2, got %v, exp %v", i, got, wantHex)
if have, want := intSample.String(), bigSample.String(); have != want {
t.Fatalf("test %d String(), have %v, want %v", i, have, want)
}
if got, _ := intSample.MarshalText(); wantHex != string(got) {
t.Fatalf("test %d #3, got %v, exp %v", i, got, wantHex)
{
have, _ := intSample.MarshalText()
want, _ := bigSample.MarshalText()
if !bytes.Equal(have, want) {
t.Fatalf("test %d MarshalText, have %q, want %q", i, have, want)
}
}
if got, _ := intSample.Value(); wantDec != got.(string) {
t.Fatalf("test %d #4, got %v, exp %v", i, got, wantHex)
{
have, _ := intSample.MarshalJSON()
want := []byte(fmt.Sprintf(`"%s"`, bigSample.Text(10)))
if !bytes.Equal(have, want) {
t.Fatalf("test %d MarshalJSON, have %q, want %q", i, have, want)
}
}
if have, _ := intSample.Value(); wantDec != have.(string) {
t.Fatalf("test %d #4, got %v, exp %v", i, have, wantHex)
}
if got := intSample.Dec(); wantDec != got {
t.Fatalf("test %d #5, got %v, exp %v", i, got, wantHex)
if have, want := intSample.Dec(), wantDec; have != want {
t.Fatalf("test %d Dec(), have %v, want %v", i, have, want)
}
{ // Json
jsonEncoded, err := json.Marshal(&jsonStruct{&intSample})
if err != nil {
t.Fatalf("test %d #6, err: %v", i, err)
t.Fatalf("test %d: json encoding err: %v", i, err)
}
jsonEncodedBig, _ := json.Marshal(&jsonBigStruct{&bigSample})
var jsonDecoded jsonStruct
err = json.Unmarshal(jsonEncoded, &jsonDecoded)
if err != nil {
t.Fatalf("test %d #7, err: %v", i, err)
t.Fatalf("test %d error unmarshaling: %v", i, err)
}
if jsonDecoded.Foo.Cmp(&intSample) != 0 {
t.Fatalf("test %d #8, have %v, want %v", i, jsonDecoded.Foo, intSample)
}
// See if we can also unmarshal from big.Int's non-string format
err = json.Unmarshal(jsonEncodedBig, &jsonDecoded)
if err != nil {
t.Fatalf("test %d unmarshalling from big.Int err: %v", i, err)
}
if jsonDecoded.Foo.Cmp(&intSample) != 0 {
t.Fatalf("test %d #8, got %v, exp %v", i, jsonDecoded.Foo, intSample)
t.Fatalf("test %d have %v, want %v", i, jsonDecoded.Foo, intSample)
}
}
// Decoding
Expand Down

0 comments on commit f24ed59

Please sign in to comment.