Skip to content

Commit

Permalink
Merge pull request #77 from hyperledger/more-json-number-support
Browse files Browse the repository at this point in the history
Increase handling of JSON Number large numbers (vs. string)
  • Loading branch information
EnriqueL8 authored Aug 28, 2024
2 parents e43c6db + 2fe278d commit 27d2240
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 82 deletions.
4 changes: 4 additions & 0 deletions internal/signermsgs/en_error_messges.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,8 @@ var (
MsgInvalidEIP155TransactionV = ffe("FF22085", "Invalid V value from EIP-155 transaction (chainId=%d)")
MsgInvalidChainID = ffe("FF22086", "Invalid chainId expected=%d actual=%d")
MsgSigningInvalidCompactRSV = ffe("FF22087", "Invalid signature data (compact R,S,V) length=%d (expected=65)")
MsgInvalidNumberString = ffe("FF22088", "Invalid integer string '%s'")
MsgInvalidIntPrecisionLoss = ffe("FF22089", "String %s cannot be converted to integer without losing precision")
MsgInvalidUint64PrecisionLoss = ffe("FF22090", "String %s cannot be converted to a uint64 without losing precision")
MsgInvalidJSONTypeForBigInt = ffe("FF22091", "JSON parsed '%T' cannot be converted to an integer")
)
65 changes: 24 additions & 41 deletions pkg/abi/inputparsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ package abi
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"reflect"
"strings"

"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly-signer/internal/signermsgs"
"github.com/hyperledger/firefly-signer/pkg/ethtypes"
)

var (
Expand Down Expand Up @@ -128,68 +130,49 @@ func getFloat64IfConvertible(v interface{}) (float64, bool) {
// with a focus on those generated by the result of an Unmarshal using Go's default
// unmarshalling.
func getIntegerFromInterface(ctx context.Context, desc string, v interface{}) (*big.Int, error) {
i := new(big.Int)
switch vt := v.(type) {
case json.Number:
i, err := ethtypes.BigIntegerFromString(ctx, vt.String())
if err != nil {
return nil, i18n.WrapError(ctx, err, signermsgs.MsgInvalidIntegerABIInput, vt, v, desc)
}
return i, nil
case string:
// We use Go's default '0' base integer parsing, where `0x` means hex,
// no prefix means decimal etc.
i, ok := i.SetString(vt, 0)
if !ok {
f, _, err := big.ParseFloat(vt, 10, 256, big.ToNearestEven)
if err != nil {
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidIntegerABIInput, vt, v, desc)
}
i, accuracy := f.Int(i)
if accuracy != big.Exact {
// If we weren't able to decode without losing precision, return an error
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidIntegerABIInput, vt, v, desc)
}

return i, nil
i, err := ethtypes.BigIntegerFromString(ctx, vt)
if err != nil {
return nil, i18n.WrapError(ctx, err, signermsgs.MsgInvalidIntegerABIInput, vt, v, desc)
}
return i, nil
case *big.Float:
i, _ := vt.Int(i)
i, _ := vt.Int(nil)
return i, nil
case *big.Int:
return vt, nil
case float64:
// This is how JSON numbers come in (no distinction between integers/floats)
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetInt64(int64(vt)), nil
case float32:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetInt64(int64(vt)), nil
case int64:
i.SetInt64(vt)
return i, nil
return new(big.Int).SetInt64(vt), nil
case int32:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetInt64(int64(vt)), nil
case int16:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetInt64(int64(vt)), nil
case int8:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetInt64(int64(vt)), nil
case int:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetInt64(int64(vt)), nil
case uint64:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetUint64(vt), nil
case uint32:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetInt64(int64(vt)), nil
case uint16:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetInt64(int64(vt)), nil
case uint8:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetInt64(int64(vt)), nil
case uint:
i.SetInt64(int64(vt))
return i, nil
return new(big.Int).SetUint64(uint64(vt)), nil
default:
if str, ok := getStringIfConvertible(v); ok {
return getIntegerFromInterface(ctx, desc, str)
Expand Down
29 changes: 9 additions & 20 deletions pkg/ethtypes/hexinteger.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -18,7 +18,6 @@ package ethtypes

import (
"context"
"encoding/json"
"fmt"
"math/big"

Expand All @@ -37,25 +36,15 @@ func (h HexInteger) MarshalJSON() ([]byte, error) {
}

func (h *HexInteger) UnmarshalJSON(b []byte) error {
var i interface{}
_ = json.Unmarshal(b, &i)
switch i := i.(type) {
case float64:
*h = HexInteger(*big.NewInt(int64(i)))
return nil
case string:
bi, ok := new(big.Int).SetString(i, 0)
if !ok {
return fmt.Errorf("unable to parse integer: %s", i)
}
if bi.Sign() < 0 {
return fmt.Errorf("negative values are not supported: %s", i)
}
*h = HexInteger(*bi)
return nil
default:
return fmt.Errorf("unable to parse integer from type %T", i)
bi, err := UnmarshalBigInt(context.Background(), b)
if err != nil {
return err
}
if bi.Sign() < 0 {
return fmt.Errorf("negative values are not supported: %s", b)
}
*h = HexInteger(*bi)
return nil
}

func (h *HexInteger) BigInt() *big.Int {
Expand Down
7 changes: 5 additions & 2 deletions pkg/ethtypes/hexinteger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ func TestHexIntegerMissingBytes(t *testing.T) {
}`

err := json.Unmarshal([]byte(testData), &testStruct)
assert.Regexp(t, "unable to parse integer", err)
assert.Regexp(t, "FF22088", err)

err = testStruct.I1.UnmarshalJSON([]byte(`{!badJSON`))
assert.Regexp(t, "invalid", err)
}

func TestHexIntegerBadType(t *testing.T) {
Expand All @@ -87,7 +90,7 @@ func TestHexIntegerBadType(t *testing.T) {
}`

err := json.Unmarshal([]byte(testData), &testStruct)
assert.Regexp(t, "unable to parse integer", err)
assert.Regexp(t, "FF22091", err)
}

func TestHexIntegerBadJSON(t *testing.T) {
Expand Down
25 changes: 9 additions & 16 deletions pkg/ethtypes/hexuint64.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ package ethtypes

import (
"context"
"encoding/json"
"fmt"
"strconv"

"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly-signer/internal/signermsgs"
)

// HexUint64 is a positive integer - serializes to JSON as an 0x hex string (no leading zeros), and parses flexibly depending on the prefix (so 0x for hex, or base 10 for plain string / float64)
Expand All @@ -40,22 +40,15 @@ func (h HexUint64) MarshalJSON() ([]byte, error) {
}

func (h *HexUint64) UnmarshalJSON(b []byte) error {
var i interface{}
_ = json.Unmarshal(b, &i)
switch i := i.(type) {
case float64:
*h = HexUint64(i)
return nil
case string:
i64, err := strconv.ParseUint(i, 0, 64)
if err != nil {
return fmt.Errorf("unable to parse integer: %s", i)
}
*h = HexUint64(i64)
return nil
default:
return fmt.Errorf("unable to parse integer from type %T", i)
bi, err := UnmarshalBigInt(context.Background(), b)
if err != nil {
return err
}
if !bi.IsUint64() {
return i18n.NewError(context.Background(), signermsgs.MsgInvalidUint64PrecisionLoss, b)
}
*h = HexUint64(bi.Uint64())
return nil
}

func (h HexUint64) Uint64() uint64 {
Expand Down
20 changes: 17 additions & 3 deletions pkg/ethtypes/hexuint64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestHexUint64MissingBytes(t *testing.T) {
}`

err := json.Unmarshal([]byte(testData), &testStruct)
assert.Regexp(t, "unable to parse integer", err)
assert.Regexp(t, "FF22088", err)
}

func TestHexUint64BadType(t *testing.T) {
Expand All @@ -87,7 +87,7 @@ func TestHexUint64BadType(t *testing.T) {
}`

err := json.Unmarshal([]byte(testData), &testStruct)
assert.Regexp(t, "unable to parse integer", err)
assert.Regexp(t, "FF22091", err)
}

func TestHexUint64BadJSON(t *testing.T) {
Expand Down Expand Up @@ -115,7 +115,21 @@ func TestHexUint64BadNegative(t *testing.T) {
}`

err := json.Unmarshal([]byte(testData), &testStruct)
assert.Regexp(t, "parse", err)
assert.Regexp(t, "FF22090", err)
}

func TestHexUint64BadTooLarge(t *testing.T) {

testStruct := struct {
I1 HexUint64 `json:"i1"`
}{}

testData := `{
"i1": "18446744073709551616"
}`

err := json.Unmarshal([]byte(testData), &testStruct)
assert.Regexp(t, "FF22090", err)
}

func TestHexUint64Constructor(t *testing.T) {
Expand Down
67 changes: 67 additions & 0 deletions pkg/ethtypes/integer_parsing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ethtypes

import (
"bytes"
"context"
"encoding/json"
"math/big"

"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly-common/pkg/log"
"github.com/hyperledger/firefly-signer/internal/signermsgs"
)

func BigIntegerFromString(ctx context.Context, s string) (*big.Int, error) {
// We use Go's default '0' base integer parsing, where `0x` means hex,
// no prefix means decimal etc.
i, ok := new(big.Int).SetString(s, 0)
if !ok {
f, _, err := big.ParseFloat(s, 10, 256, big.ToNearestEven)
if err != nil {
log.L(ctx).Errorf("Error parsing numeric string '%s': %s", s, err)
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidNumberString, s)
}
i, accuracy := f.Int(i)
if accuracy != big.Exact {
// If we weren't able to decode without losing precision, return an error
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidIntPrecisionLoss, s)
}

return i, nil
}
return i, nil
}

func UnmarshalBigInt(ctx context.Context, b []byte) (*big.Int, error) {
var i interface{}
d := json.NewDecoder(bytes.NewReader(b))
d.UseNumber()
err := d.Decode(&i)
if err != nil {
return nil, err
}
switch i := i.(type) {
case json.Number:
return BigIntegerFromString(context.Background(), i.String())
case string:
return BigIntegerFromString(context.Background(), i)
default:
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidJSONTypeForBigInt, i)
}
}
46 changes: 46 additions & 0 deletions pkg/ethtypes/integer_parsing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ethtypes

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
)

func TestIntegerParsing(t *testing.T) {
ctx := context.Background()

i, err := BigIntegerFromString(ctx, "1.0000000000000000000000001e+25")
assert.NoError(t, err)
assert.Equal(t, "10000000000000000000000001", i.String())

i, err = BigIntegerFromString(ctx, "10000000000000000000000000000001")
assert.NoError(t, err)
assert.Equal(t, "10000000000000000000000000000001", i.String())

i, err = BigIntegerFromString(ctx, "20000000000000000000000000000002")
assert.NoError(t, err)
assert.Equal(t, "20000000000000000000000000000002", i.String())

_, err = BigIntegerFromString(ctx, "0xGG")
assert.Regexp(t, "FF22088", err)

_, err = BigIntegerFromString(ctx, "3.0000000000000000000000000000003")
assert.Regexp(t, "FF22089", err)
}

0 comments on commit 27d2240

Please sign in to comment.