Skip to content
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
17 changes: 17 additions & 0 deletions pkg/ethtypes/hexinteger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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')))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah this is cleaver stuff the c - '0' to convert a byte to numeric

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
Expand Down
32 changes: 32 additions & 0 deletions pkg/ethtypes/hexinteger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 14 additions & 8 deletions pkg/ethtypes/integer_parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
14 changes: 14 additions & 0 deletions pkg/ethtypes/integer_parsing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package ethtypes

import (
"context"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -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)
}