diff --git a/pkg/ethtypes/hexinteger.go b/pkg/ethtypes/hexinteger.go index c734c47..c1e2324 100644 --- a/pkg/ethtypes/hexinteger.go +++ b/pkg/ethtypes/hexinteger.go @@ -36,6 +36,23 @@ func (h HexInteger) MarshalJSON() ([]byte, error) { } func (h *HexInteger) UnmarshalJSON(b []byte) error { + // Fastpath for quoted single-digit hex values "0x0"–"0xf": skip the + // json.Decoder, bytes.Reader, and json.Number allocations inside UnmarshalBigInt. + // Covers the dominant case where trace value fields are zero (non-value calls). + if len(b) == 5 && b[0] == '"' && b[1] == '0' && b[2] == 'x' && b[4] == '"' { + c := b[3] + switch { + case c == '0': + *h = HexInteger{} // zero big.Int has nil abs — no allocation + return nil + case c >= '1' && c <= '9': + *h = HexInteger(*big.NewInt(int64(c - '0'))) + return nil + case c >= 'a' && c <= 'f': + *h = HexInteger(*big.NewInt(int64(c-'a') + 10)) + return nil + } + } bi, err := UnmarshalBigInt(context.Background(), b) if err != nil { return err diff --git a/pkg/ethtypes/hexinteger_test.go b/pkg/ethtypes/hexinteger_test.go index 8a3d611..7e9ece3 100644 --- a/pkg/ethtypes/hexinteger_test.go +++ b/pkg/ethtypes/hexinteger_test.go @@ -62,6 +62,38 @@ func TestHexIntegerOk(t *testing.T) { } +func TestHexIntegerFastpath(t *testing.T) { + // Zero — no-allocation path + var h0 HexInteger + assert.NoError(t, h0.UnmarshalJSON([]byte(`"0x0"`))) + assert.Equal(t, int64(0), h0.BigInt().Int64()) + assert.Equal(t, "0x0", h0.String()) + + // Decimal digits 1–9 + for c := byte('1'); c <= '9'; c++ { + var h HexInteger + assert.NoError(t, h.UnmarshalJSON([]byte{'"', '0', 'x', c, '"'})) + assert.Equal(t, int64(c-'0'), h.BigInt().Int64()) + } + + // Hex digits a–f + for i, c := range []byte("abcdef") { + var h HexInteger + assert.NoError(t, h.UnmarshalJSON([]byte{'"', '0', 'x', c, '"'})) + assert.Equal(t, int64(10+i), h.BigInt().Int64()) + } + + // Uppercase falls through to slow path but still succeeds + var hUpper HexInteger + assert.NoError(t, hUpper.UnmarshalJSON([]byte(`"0xA"`))) + assert.Equal(t, int64(10), hUpper.BigInt().Int64()) + + // Multi-digit bypasses fastpath and succeeds normally + var hMulti HexInteger + assert.NoError(t, hMulti.UnmarshalJSON([]byte(`"0xff"`))) + assert.Equal(t, int64(255), hMulti.BigInt().Int64()) +} + func TestHexIntegerMissingBytes(t *testing.T) { testStruct := struct { diff --git a/pkg/ethtypes/integer_parsing.go b/pkg/ethtypes/integer_parsing.go index f2f5427..5590396 100644 --- a/pkg/ethtypes/integer_parsing.go +++ b/pkg/ethtypes/integer_parsing.go @@ -32,18 +32,24 @@ func BigIntegerFromString(ctx context.Context, s string) (*big.Int, error) { // 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) + // Fallback for decimal floats and scientific notation (e.g. "12345.0", "1e10"). + // big.Rat gives exact rational arithmetic — no precision limit or rounding mode. + // Guard length before SetString to prevent unbounded memory use (CVE-2022-23772). + // uint256 max is 78 decimal digits; float notation adds at most a sign, decimal + // point, and exponent ("e+77"), so any valid value fits within 100 characters. + if len(s) > 100 { + log.L(ctx).Errorf("Error parsing numeric string '%s'", s) 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 + r, ok := new(big.Rat).SetString(s) //nolint:gosec // G113: input bounded to 100 chars by the guard above + if !ok { + log.L(ctx).Errorf("Error parsing numeric string '%s'", s) + return nil, i18n.NewError(ctx, signermsgs.MsgInvalidNumberString, s) + } + if !r.IsInt() { return nil, i18n.NewError(ctx, signermsgs.MsgInvalidIntPrecisionLoss, s) } - - return i, nil + return r.Num(), nil } return i, nil } diff --git a/pkg/ethtypes/integer_parsing_test.go b/pkg/ethtypes/integer_parsing_test.go index b5e4c06..774cc14 100644 --- a/pkg/ethtypes/integer_parsing_test.go +++ b/pkg/ethtypes/integer_parsing_test.go @@ -18,6 +18,7 @@ package ethtypes import ( "context" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -43,4 +44,17 @@ func TestIntegerParsing(t *testing.T) { _, err = BigIntegerFromString(ctx, "3.0000000000000000000000000000003") assert.Regexp(t, "FF22089", err) + + // big.Rat handles e-notation exactly — "1e10" is 10^10, no float rounding + i, err = BigIntegerFromString(ctx, "1e10") + assert.NoError(t, err) + assert.Equal(t, "10000000000", i.String()) + + // Non-integer rational is a precision-loss error + _, err = BigIntegerFromString(ctx, "1.5") + assert.Regexp(t, "FF22089", err) + + // String exceeding 100 chars is rejected before Rat.SetString (CVE-2022-23772 guard) + _, err = BigIntegerFromString(ctx, "1."+strings.Repeat("0", 99)+"1e+100") + assert.Regexp(t, "FF22088", err) }