Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase handling of JSON Number large numbers (vs. string) #77

Merged
merged 3 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
peterbroadhurst marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Loading