Skip to content

Commit 27d2240

Browse files
authored
Merge pull request #77 from hyperledger/more-json-number-support
Increase handling of JSON Number large numbers (vs. string)
2 parents e43c6db + 2fe278d commit 27d2240

File tree

8 files changed

+181
-82
lines changed

8 files changed

+181
-82
lines changed

internal/signermsgs/en_error_messges.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,8 @@ var (
104104
MsgInvalidEIP155TransactionV = ffe("FF22085", "Invalid V value from EIP-155 transaction (chainId=%d)")
105105
MsgInvalidChainID = ffe("FF22086", "Invalid chainId expected=%d actual=%d")
106106
MsgSigningInvalidCompactRSV = ffe("FF22087", "Invalid signature data (compact R,S,V) length=%d (expected=65)")
107+
MsgInvalidNumberString = ffe("FF22088", "Invalid integer string '%s'")
108+
MsgInvalidIntPrecisionLoss = ffe("FF22089", "String %s cannot be converted to integer without losing precision")
109+
MsgInvalidUint64PrecisionLoss = ffe("FF22090", "String %s cannot be converted to a uint64 without losing precision")
110+
MsgInvalidJSONTypeForBigInt = ffe("FF22091", "JSON parsed '%T' cannot be converted to an integer")
107111
)

pkg/abi/inputparsing.go

Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ package abi
1919
import (
2020
"context"
2121
"encoding/hex"
22+
"encoding/json"
2223
"fmt"
2324
"math/big"
2425
"reflect"
2526
"strings"
2627

2728
"github.com/hyperledger/firefly-common/pkg/i18n"
2829
"github.com/hyperledger/firefly-signer/internal/signermsgs"
30+
"github.com/hyperledger/firefly-signer/pkg/ethtypes"
2931
)
3032

3133
var (
@@ -128,68 +130,49 @@ func getFloat64IfConvertible(v interface{}) (float64, bool) {
128130
// with a focus on those generated by the result of an Unmarshal using Go's default
129131
// unmarshalling.
130132
func getIntegerFromInterface(ctx context.Context, desc string, v interface{}) (*big.Int, error) {
131-
i := new(big.Int)
132133
switch vt := v.(type) {
134+
case json.Number:
135+
i, err := ethtypes.BigIntegerFromString(ctx, vt.String())
136+
if err != nil {
137+
return nil, i18n.WrapError(ctx, err, signermsgs.MsgInvalidIntegerABIInput, vt, v, desc)
138+
}
139+
return i, nil
133140
case string:
134-
// We use Go's default '0' base integer parsing, where `0x` means hex,
135-
// no prefix means decimal etc.
136-
i, ok := i.SetString(vt, 0)
137-
if !ok {
138-
f, _, err := big.ParseFloat(vt, 10, 256, big.ToNearestEven)
139-
if err != nil {
140-
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidIntegerABIInput, vt, v, desc)
141-
}
142-
i, accuracy := f.Int(i)
143-
if accuracy != big.Exact {
144-
// If we weren't able to decode without losing precision, return an error
145-
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidIntegerABIInput, vt, v, desc)
146-
}
147-
148-
return i, nil
141+
i, err := ethtypes.BigIntegerFromString(ctx, vt)
142+
if err != nil {
143+
return nil, i18n.WrapError(ctx, err, signermsgs.MsgInvalidIntegerABIInput, vt, v, desc)
149144
}
150145
return i, nil
151146
case *big.Float:
152-
i, _ := vt.Int(i)
147+
i, _ := vt.Int(nil)
153148
return i, nil
154149
case *big.Int:
155150
return vt, nil
156151
case float64:
157152
// This is how JSON numbers come in (no distinction between integers/floats)
158-
i.SetInt64(int64(vt))
159-
return i, nil
153+
return new(big.Int).SetInt64(int64(vt)), nil
160154
case float32:
161-
i.SetInt64(int64(vt))
162-
return i, nil
155+
return new(big.Int).SetInt64(int64(vt)), nil
163156
case int64:
164-
i.SetInt64(vt)
165-
return i, nil
157+
return new(big.Int).SetInt64(vt), nil
166158
case int32:
167-
i.SetInt64(int64(vt))
168-
return i, nil
159+
return new(big.Int).SetInt64(int64(vt)), nil
169160
case int16:
170-
i.SetInt64(int64(vt))
171-
return i, nil
161+
return new(big.Int).SetInt64(int64(vt)), nil
172162
case int8:
173-
i.SetInt64(int64(vt))
174-
return i, nil
163+
return new(big.Int).SetInt64(int64(vt)), nil
175164
case int:
176-
i.SetInt64(int64(vt))
177-
return i, nil
165+
return new(big.Int).SetInt64(int64(vt)), nil
178166
case uint64:
179-
i.SetInt64(int64(vt))
180-
return i, nil
167+
return new(big.Int).SetUint64(vt), nil
181168
case uint32:
182-
i.SetInt64(int64(vt))
183-
return i, nil
169+
return new(big.Int).SetInt64(int64(vt)), nil
184170
case uint16:
185-
i.SetInt64(int64(vt))
186-
return i, nil
171+
return new(big.Int).SetInt64(int64(vt)), nil
187172
case uint8:
188-
i.SetInt64(int64(vt))
189-
return i, nil
173+
return new(big.Int).SetInt64(int64(vt)), nil
190174
case uint:
191-
i.SetInt64(int64(vt))
192-
return i, nil
175+
return new(big.Int).SetUint64(uint64(vt)), nil
193176
default:
194177
if str, ok := getStringIfConvertible(v); ok {
195178
return getIntegerFromInterface(ctx, desc, str)

pkg/ethtypes/hexinteger.go

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2023 Kaleido, Inc.
1+
// Copyright © 2024 Kaleido, Inc.
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
//
@@ -18,7 +18,6 @@ package ethtypes
1818

1919
import (
2020
"context"
21-
"encoding/json"
2221
"fmt"
2322
"math/big"
2423

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

3938
func (h *HexInteger) UnmarshalJSON(b []byte) error {
40-
var i interface{}
41-
_ = json.Unmarshal(b, &i)
42-
switch i := i.(type) {
43-
case float64:
44-
*h = HexInteger(*big.NewInt(int64(i)))
45-
return nil
46-
case string:
47-
bi, ok := new(big.Int).SetString(i, 0)
48-
if !ok {
49-
return fmt.Errorf("unable to parse integer: %s", i)
50-
}
51-
if bi.Sign() < 0 {
52-
return fmt.Errorf("negative values are not supported: %s", i)
53-
}
54-
*h = HexInteger(*bi)
55-
return nil
56-
default:
57-
return fmt.Errorf("unable to parse integer from type %T", i)
39+
bi, err := UnmarshalBigInt(context.Background(), b)
40+
if err != nil {
41+
return err
42+
}
43+
if bi.Sign() < 0 {
44+
return fmt.Errorf("negative values are not supported: %s", b)
5845
}
46+
*h = HexInteger(*bi)
47+
return nil
5948
}
6049

6150
func (h *HexInteger) BigInt() *big.Int {

pkg/ethtypes/hexinteger_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ func TestHexIntegerMissingBytes(t *testing.T) {
7373
}`
7474

7575
err := json.Unmarshal([]byte(testData), &testStruct)
76-
assert.Regexp(t, "unable to parse integer", err)
76+
assert.Regexp(t, "FF22088", err)
77+
78+
err = testStruct.I1.UnmarshalJSON([]byte(`{!badJSON`))
79+
assert.Regexp(t, "invalid", err)
7780
}
7881

7982
func TestHexIntegerBadType(t *testing.T) {
@@ -87,7 +90,7 @@ func TestHexIntegerBadType(t *testing.T) {
8790
}`
8891

8992
err := json.Unmarshal([]byte(testData), &testStruct)
90-
assert.Regexp(t, "unable to parse integer", err)
93+
assert.Regexp(t, "FF22091", err)
9194
}
9295

9396
func TestHexIntegerBadJSON(t *testing.T) {

pkg/ethtypes/hexuint64.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ package ethtypes
1818

1919
import (
2020
"context"
21-
"encoding/json"
2221
"fmt"
2322
"strconv"
2423

2524
"github.com/hyperledger/firefly-common/pkg/i18n"
25+
"github.com/hyperledger/firefly-signer/internal/signermsgs"
2626
)
2727

2828
// 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)
@@ -40,22 +40,15 @@ func (h HexUint64) MarshalJSON() ([]byte, error) {
4040
}
4141

4242
func (h *HexUint64) UnmarshalJSON(b []byte) error {
43-
var i interface{}
44-
_ = json.Unmarshal(b, &i)
45-
switch i := i.(type) {
46-
case float64:
47-
*h = HexUint64(i)
48-
return nil
49-
case string:
50-
i64, err := strconv.ParseUint(i, 0, 64)
51-
if err != nil {
52-
return fmt.Errorf("unable to parse integer: %s", i)
53-
}
54-
*h = HexUint64(i64)
55-
return nil
56-
default:
57-
return fmt.Errorf("unable to parse integer from type %T", i)
43+
bi, err := UnmarshalBigInt(context.Background(), b)
44+
if err != nil {
45+
return err
46+
}
47+
if !bi.IsUint64() {
48+
return i18n.NewError(context.Background(), signermsgs.MsgInvalidUint64PrecisionLoss, b)
5849
}
50+
*h = HexUint64(bi.Uint64())
51+
return nil
5952
}
6053

6154
func (h HexUint64) Uint64() uint64 {

pkg/ethtypes/hexuint64_test.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func TestHexUint64MissingBytes(t *testing.T) {
7373
}`
7474

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

7979
func TestHexUint64BadType(t *testing.T) {
@@ -87,7 +87,7 @@ func TestHexUint64BadType(t *testing.T) {
8787
}`
8888

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

9393
func TestHexUint64BadJSON(t *testing.T) {
@@ -115,7 +115,21 @@ func TestHexUint64BadNegative(t *testing.T) {
115115
}`
116116

117117
err := json.Unmarshal([]byte(testData), &testStruct)
118-
assert.Regexp(t, "parse", err)
118+
assert.Regexp(t, "FF22090", err)
119+
}
120+
121+
func TestHexUint64BadTooLarge(t *testing.T) {
122+
123+
testStruct := struct {
124+
I1 HexUint64 `json:"i1"`
125+
}{}
126+
127+
testData := `{
128+
"i1": "18446744073709551616"
129+
}`
130+
131+
err := json.Unmarshal([]byte(testData), &testStruct)
132+
assert.Regexp(t, "FF22090", err)
119133
}
120134

121135
func TestHexUint64Constructor(t *testing.T) {

pkg/ethtypes/integer_parsing.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright © 2024 Kaleido, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package ethtypes
18+
19+
import (
20+
"bytes"
21+
"context"
22+
"encoding/json"
23+
"math/big"
24+
25+
"github.com/hyperledger/firefly-common/pkg/i18n"
26+
"github.com/hyperledger/firefly-common/pkg/log"
27+
"github.com/hyperledger/firefly-signer/internal/signermsgs"
28+
)
29+
30+
func BigIntegerFromString(ctx context.Context, s string) (*big.Int, error) {
31+
// We use Go's default '0' base integer parsing, where `0x` means hex,
32+
// no prefix means decimal etc.
33+
i, ok := new(big.Int).SetString(s, 0)
34+
if !ok {
35+
f, _, err := big.ParseFloat(s, 10, 256, big.ToNearestEven)
36+
if err != nil {
37+
log.L(ctx).Errorf("Error parsing numeric string '%s': %s", s, err)
38+
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidNumberString, s)
39+
}
40+
i, accuracy := f.Int(i)
41+
if accuracy != big.Exact {
42+
// If we weren't able to decode without losing precision, return an error
43+
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidIntPrecisionLoss, s)
44+
}
45+
46+
return i, nil
47+
}
48+
return i, nil
49+
}
50+
51+
func UnmarshalBigInt(ctx context.Context, b []byte) (*big.Int, error) {
52+
var i interface{}
53+
d := json.NewDecoder(bytes.NewReader(b))
54+
d.UseNumber()
55+
err := d.Decode(&i)
56+
if err != nil {
57+
return nil, err
58+
}
59+
switch i := i.(type) {
60+
case json.Number:
61+
return BigIntegerFromString(context.Background(), i.String())
62+
case string:
63+
return BigIntegerFromString(context.Background(), i)
64+
default:
65+
return nil, i18n.NewError(ctx, signermsgs.MsgInvalidJSONTypeForBigInt, i)
66+
}
67+
}

pkg/ethtypes/integer_parsing_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright © 2024 Kaleido, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package ethtypes
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestIntegerParsing(t *testing.T) {
27+
ctx := context.Background()
28+
29+
i, err := BigIntegerFromString(ctx, "1.0000000000000000000000001e+25")
30+
assert.NoError(t, err)
31+
assert.Equal(t, "10000000000000000000000001", i.String())
32+
33+
i, err = BigIntegerFromString(ctx, "10000000000000000000000000000001")
34+
assert.NoError(t, err)
35+
assert.Equal(t, "10000000000000000000000000000001", i.String())
36+
37+
i, err = BigIntegerFromString(ctx, "20000000000000000000000000000002")
38+
assert.NoError(t, err)
39+
assert.Equal(t, "20000000000000000000000000000002", i.String())
40+
41+
_, err = BigIntegerFromString(ctx, "0xGG")
42+
assert.Regexp(t, "FF22088", err)
43+
44+
_, err = BigIntegerFromString(ctx, "3.0000000000000000000000000000003")
45+
assert.Regexp(t, "FF22089", err)
46+
}

0 commit comments

Comments
 (0)