From 4be692193eb9d0387af57004f78804c1b41d674c Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 27 Jul 2023 18:12:02 +0300 Subject: [PATCH] vm: allow parsing scientific JSON numbers 52-bit precision is not enough for our 256-bit VM, but this value matches the reference implementation, see the https://github.com/neo-project/neo/issues/2879. MaxIntegerPrec will be increased (or even removed) as soon as the ref. issue is resolved. Signed-off-by: Anna Shaleva --- pkg/vm/stackitem/json.go | 42 ++++++++++++++++++++++++++--------- pkg/vm/stackitem/json_test.go | 30 +++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/pkg/vm/stackitem/json.go b/pkg/vm/stackitem/json.go index d7b93f623..508d91fbd 100644 --- a/pkg/vm/stackitem/json.go +++ b/pkg/vm/stackitem/json.go @@ -26,6 +26,11 @@ const MaxAllowedInteger = 2<<53 - 1 // MaxJSONDepth is the maximum allowed nesting level of an encoded/decoded JSON. const MaxJSONDepth = 10 +// MaxIntegerPrec is the maximum precision allowed for big.Integer parsing. +// It equals to the reference value and doesn't allow to precisely parse big +// numbers, see the https://github.com/neo-project/neo/issues/2879. +const MaxIntegerPrec = 53 + // ErrInvalidValue is returned when an item value doesn't fit some constraints // during serialization or deserialization. var ErrInvalidValue = errors.New("invalid value") @@ -213,18 +218,35 @@ func (d *decoder) decode() (Item, error) { return NewByteArray([]byte(t)), nil case json.Number: ts := t.String() - dot := strings.IndexByte(ts, '.') - if dot != -1 { - // As a special case numbers like 123.000 are allowed (SetString rejects them). - // And yes, that's the way C# code works also. - for _, r := range ts[dot+1:] { - if r != '0' { - return nil, fmt.Errorf("%w (real value for int)", ErrInvalidValue) - } + var ( + num *big.Int + ok bool + ) + isScientific := strings.Contains(ts, "e+") || strings.Contains(ts, "E+") + if isScientific { + // As a special case numbers like 2.8e+22 are allowed (SetString rejects them). + // That's the way how C# code works. + f, _, err := big.ParseFloat(ts, 10, MaxIntegerPrec, big.ToNearestEven) + if err != nil { + return nil, fmt.Errorf("%w (malformed exp value for int)", ErrInvalidValue) } - ts = ts[:dot] + num = new(big.Int) + _, acc := f.Int(num) + ok = acc == big.Exact + } else { + dot := strings.IndexByte(ts, '.') + if dot != -1 { + // As a special case numbers like 123.000 are allowed (SetString rejects them). + // And yes, that's the way C# code works also. + for _, r := range ts[dot+1:] { + if r != '0' { + return nil, fmt.Errorf("%w (real value for int)", ErrInvalidValue) + } + } + ts = ts[:dot] + } + num, ok = new(big.Int).SetString(ts, 10) } - num, ok := new(big.Int).SetString(ts, 10) if !ok { return nil, fmt.Errorf("%w (integer)", ErrInvalidValue) } diff --git a/pkg/vm/stackitem/json_test.go b/pkg/vm/stackitem/json_test.go index 59cc1d1a0..679da1b3b 100644 --- a/pkg/vm/stackitem/json_test.go +++ b/pkg/vm/stackitem/json_test.go @@ -8,7 +8,11 @@ import ( "github.com/stretchr/testify/require" ) -func getTestDecodeFunc(js string, expected ...any) func(t *testing.T) { +func getTestDecodeFunc(js string, expected ...interface{}) func(t *testing.T) { + return getTestDecodeEncodeFunc(js, true, expected...) +} + +func getTestDecodeEncodeFunc(js string, needEncode bool, expected ...interface{}) func(t *testing.T) { return func(t *testing.T) { actual, err := FromJSON([]byte(js), 20) if expected[0] == nil { @@ -18,7 +22,7 @@ func getTestDecodeFunc(js string, expected ...any) func(t *testing.T) { require.NoError(t, err) require.Equal(t, Make(expected[0]), actual) - if len(expected) == 1 { + if needEncode && len(expected) == 1 { encoded, err := ToJSON(actual) require.NoError(t, err) require.Equal(t, js, string(encoded)) @@ -27,6 +31,8 @@ func getTestDecodeFunc(js string, expected ...any) func(t *testing.T) { } func TestFromToJSON(t *testing.T) { + bigInt, ok := new(big.Int).SetString("28000000000000000000000", 10) + require.True(t, ok) t.Run("ByteString", func(t *testing.T) { t.Run("Empty", getTestDecodeFunc(`""`, []byte{})) t.Run("Base64", getTestDecodeFunc(`"test"`, "test")) @@ -35,6 +41,8 @@ func TestFromToJSON(t *testing.T) { t.Run("BigInteger", func(t *testing.T) { t.Run("ZeroFloat", getTestDecodeFunc(`12.000`, 12, nil)) t.Run("NonZeroFloat", getTestDecodeFunc(`12.01`, nil)) + t.Run("ExpInteger", getTestDecodeEncodeFunc(`2.8e+22`, false, bigInt)) + t.Run("ExpFloat", getTestDecodeEncodeFunc(`1.2345e+3`, false, nil)) // float value, parsing should fail for it. t.Run("Negative", getTestDecodeFunc(`-4`, -4)) t.Run("Positive", getTestDecodeFunc(`123`, 123)) }) @@ -122,6 +130,24 @@ func TestFromToJSON(t *testing.T) { }) } +// TestFromJSON_CompatBigInt ensures that maximum BigInt parsing precision matches +// the C# one, ref. https://github.com/neo-project/neo/issues/2879. +func TestFromJSON_CompatBigInt(t *testing.T) { + tcs := map[string]string{ + `9.05e+28`: "90499999999999993918259200000", + `1.871e+21`: "1871000000000000000000", + `3.0366e+32`: "303660000000000004445016810323968", + `1e+30`: "1000000000000000019884624838656", + } + for in, expected := range tcs { + t.Run(in, func(t *testing.T) { + actual, err := FromJSON([]byte(in), 5) + require.NoError(t, err) + require.Equal(t, expected, actual.Value().(*big.Int).String()) + }) + } +} + func testToJSON(t *testing.T, expectedErr error, item Item) { data, err := ToJSON(item) if expectedErr != nil {