From 1947619969933702c9f1ef28ff3805a07d93be20 Mon Sep 17 00:00:00 2001 From: Alexey Kiselev Date: Mon, 18 May 2026 14:04:07 +0400 Subject: [PATCH 1/3] Add support for Unicode digit normalization in string to Int and BigInt conversion. Added conversion of Unicode decimal digits to ASCII digits and integrated it into Ride functions parseInt, parseIntValued, stringToBigInt, and stringToBigIntOpt. Test added and updated. --- pkg/ride/functions_bigint.go | 8 +++- pkg/ride/functions_bigint_test.go | 74 +++++++++++++++++++++++------- pkg/ride/functions_strings.go | 72 ++++++++++++++++++++++++++++- pkg/ride/functions_strings_test.go | 66 ++++++++++++++++++++++---- 4 files changed, 193 insertions(+), 27 deletions(-) diff --git a/pkg/ride/functions_bigint.go b/pkg/ride/functions_bigint.go index 2721e0b50b..cd4a191fac 100644 --- a/pkg/ride/functions_bigint.go +++ b/pkg/ride/functions_bigint.go @@ -11,6 +11,8 @@ import ( "github.com/wavesplatform/gowaves/pkg/util/common" ) +const base10 = 10 + var ( zeroBigInt = big.NewInt(0) ) @@ -519,7 +521,11 @@ func stringToBigInt(_ environment, args ...rideType) (rideType, error) { if l := len(s); l > 155 { // 155 symbols is the length of math.MinBigInt value is string representation return nil, errors.Errorf("stringToBigInt: string is too long (%d symbols) for a BigInt", l) } - r, ok := new(big.Int).SetString(string(s), 10) + ns, err := normalizeDigits(string(s)) + if err != nil { + return nil, errors.Wrap(err, "stringToBigInt") + } + r, ok := new(big.Int).SetString(ns, base10) if !ok { return nil, errors.Errorf("stringToBigInt: failed to convert string '%s' to BigInt", s) } diff --git a/pkg/ride/functions_bigint_test.go b/pkg/ride/functions_bigint_test.go index 8bcd38cc6d..fd47344024 100644 --- a/pkg/ride/functions_bigint_test.go +++ b/pkg/ride/functions_bigint_test.go @@ -691,7 +691,7 @@ func BenchmarkBigIntToString(b *testing.B) { func TestStringToBigInt(t *testing.T) { v, ok := new(big.Int).SetString("52785833603464895924505196455835395749861094195642486808108138863402869537852026544579466671752822414281401856143643660416162921950916138504990605852480", 10) require.True(t, ok) - for _, test := range []struct { + for i, test := range []struct { args []rideType fail bool r rideType @@ -706,24 +706,45 @@ func TestStringToBigInt(t *testing.T) { {[]rideType{rideString("9223372036854775807")}, false, toRideBigInt(math.MaxInt64)}, {[]rideType{rideString("-9223372036854775808")}, false, toRideBigInt(math.MinInt64)}, {[]rideType{rideString("52785833603464895924505196455835395749861094195642486808108138863402869537852026544579466671752822414281401856143643660416162921950916138504990605852480")}, false, rideBigInt{v: v}}, + {[]rideType{rideString("⓪①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳")}, true, nil}, + {[]rideType{rideString("ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ")}, true, nil}, + {[]rideType{rideString("ⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹ")}, true, nil}, + {[]rideType{rideString("⁰¹²³⁴⁵⁶⁷⁸⁹")}, true, nil}, + {[]rideType{rideString("₀₁₂₃₄₅₆₇₈₉")}, true, nil}, + {[]rideType{rideString("𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗")}, true, nil}, + {[]rideType{rideString("𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡")}, true, nil}, + {[]rideType{rideString("𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫")}, true, nil}, + {[]rideType{rideString("𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵")}, true, nil}, + {[]rideType{rideString("𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿")}, true, nil}, + {[]rideType{rideString("0123456789")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("٠١٢٣٤٥٦٧٨٩")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("۰۱۲۳۴۵۶۷۸۹")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("०१२३४५६७८९")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("০১২৩৪৫৬৭৮৯")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("๐๑๒๓๔๕๖๗๘๙")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("௦௧௨௩௪௫௬௭௮௯")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("၀၁၂၃၄၅၆၇၈၉")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("០១២៣៤៥៦៧៨៩")}, false, toRideBigInt(123456789)}, {[]rideType{rideString("0"), rideInt(4)}, true, nil}, {[]rideType{rideInt(0)}, true, nil}, {[]rideType{}, true, nil}, } { - r, err := stringToBigInt(nil, test.args...) - if test.fail { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.True(t, test.r.eq(r), fmt.Sprintf("%s != %s", test.r, r)) - } + t.Run(fmt.Sprintf("test_%d", i+1), func(t *testing.T) { + r, err := stringToBigInt(nil, test.args...) + if test.fail { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.True(t, test.r.eq(r), fmt.Sprintf("%s != %s", test.r, r)) + } + }) } } func TestStringToBigIntOpt(t *testing.T) { v, ok := new(big.Int).SetString("52785833603464895924505196455835395749861094195642486808108138863402869537852026544579466671752822414281401856143643660416162921950916138504990605852480", 10) require.True(t, ok) - for _, test := range []struct { + for i, test := range []struct { args []rideType fail bool r rideType @@ -738,17 +759,38 @@ func TestStringToBigIntOpt(t *testing.T) { {[]rideType{rideString("9223372036854775807")}, false, toRideBigInt(math.MaxInt64)}, {[]rideType{rideString("-9223372036854775808")}, false, toRideBigInt(math.MinInt64)}, {[]rideType{rideString("52785833603464895924505196455835395749861094195642486808108138863402869537852026544579466671752822414281401856143643660416162921950916138504990605852480")}, false, rideBigInt{v: v}}, + {[]rideType{rideString("⓪①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳")}, false, newUnit(nil)}, + {[]rideType{rideString("ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ")}, false, newUnit(nil)}, + {[]rideType{rideString("ⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹ")}, false, newUnit(nil)}, + {[]rideType{rideString("⁰¹²³⁴⁵⁶⁷⁸⁹")}, false, newUnit(nil)}, + {[]rideType{rideString("₀₁₂₃₄₅₆₇₈₉")}, false, newUnit(nil)}, + {[]rideType{rideString("𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗")}, false, newUnit(nil)}, + {[]rideType{rideString("𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡")}, false, newUnit(nil)}, + {[]rideType{rideString("𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫")}, false, newUnit(nil)}, + {[]rideType{rideString("𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵")}, false, newUnit(nil)}, + {[]rideType{rideString("𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿")}, false, newUnit(nil)}, + {[]rideType{rideString("0123456789")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("٠١٢٣٤٥٦٧٨٩")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("۰۱۲۳۴۵۶۷۸۹")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("०१२३४५६७८९")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("০১২৩৪৫৬৭৮৯")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("๐๑๒๓๔๕๖๗๘๙")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("௦௧௨௩௪௫௬௭௮௯")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("၀၁၂၃၄၅၆၇၈၉")}, false, toRideBigInt(123456789)}, + {[]rideType{rideString("០១២៣៤៥៦៧៨៩")}, false, toRideBigInt(123456789)}, {[]rideType{rideString("0"), rideInt(4)}, false, newUnit(nil)}, {[]rideType{rideInt(0)}, false, newUnit(nil)}, {[]rideType{}, false, newUnit(nil)}, } { - r, err := stringToBigIntOpt(nil, test.args...) - if test.fail { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.True(t, test.r.eq(r), fmt.Sprintf("%s != %s", test.r, r)) - } + t.Run(fmt.Sprintf("test_%d", i+1), func(t *testing.T) { + r, err := stringToBigIntOpt(nil, test.args...) + if test.fail { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.True(t, test.r.eq(r), fmt.Sprintf("%s != %s", test.r, r)) + } + }) } } diff --git a/pkg/ride/functions_strings.go b/pkg/ride/functions_strings.go index 22cd7a019e..cc152bf348 100644 --- a/pkg/ride/functions_strings.go +++ b/pkg/ride/functions_strings.go @@ -1,11 +1,14 @@ package ride import ( + "fmt" "strconv" "strings" + "unicode" "unicode/utf16" "unicode/utf8" + "github.com/ccoveille/go-safecast/v2" "github.com/pkg/errors" "github.com/wavesplatform/gowaves/pkg/proto" @@ -302,9 +305,13 @@ func parseInt(_ environment, args ...rideType) (rideType, error) { if err != nil { return nil, errors.Wrap(err, "parseInt") } - i, err := strconv.ParseInt(string(s), 10, 64) + ns, err := normalizeDigits(string(s)) if err != nil { - return rideUnit{}, nil + return rideUnit{}, nil //nolint:nilerr // Suppress invalid digits string error, return unit instead. + } + i, err := strconv.ParseInt(ns, 10, 64) + if err != nil { + return rideUnit{}, nil //nolint:nilerr // Suppress conversion error, return unit instead. } return rideInt(i), nil } @@ -317,6 +324,67 @@ func parseIntValue(env environment, args ...rideType) (rideType, error) { return extractValue(maybeInt) } +func digitValueInRange(r rune, lo, hi, stride uint32) (byte, bool) { + v, err := safecast.Convert[uint32](r) + if err != nil { + return 0, false + } + if v < lo || v > hi { + return 0, false + } + if (v-lo)%stride != 0 { + return 0, false + } + return byte((v - lo) / stride % base10), true +} + +func digitValue(r rune) (byte, bool) { + for _, rt := range unicode.Digit.R16 { + if v, ok := digitValueInRange(r, uint32(rt.Lo), uint32(rt.Hi), uint32(rt.Stride)); ok { + return v, true + } + } + for _, rt := range unicode.Digit.R32 { + if v, ok := digitValueInRange(r, rt.Lo, rt.Hi, rt.Stride); ok { + return v, true + } + } + return 0, false +} + +// isMathematicalStyleDigit returns true for Mathematical Alphanumeric Symbols like 𝟎-𝟗, 𝟘-𝟡, 𝟢-𝟫, 𝟬-𝟵, 𝟶-𝟿. +func isMathematicalStyledDigit(r rune) bool { + return '\U0001D7CE' <= r && r <= '\U0001D7FF' +} + +func normalizeDigits(s string) (string, error) { + var b strings.Builder + for i, r := range s { + switch { + case r == '+' || r == '-': + // Sign is allowed only at the beginning. + if i != 0 { + return "", fmt.Errorf("unexpected sign %q", r) + } + b.WriteRune(r) + case '0' <= r && r <= '9': + b.WriteRune(r) + case unicode.IsDigit(r): + if isMathematicalStyledDigit(r) { + return "", fmt.Errorf("unsupported styled digit %q", r) + } + v, ok := digitValue(r) + if !ok { + return "", fmt.Errorf("unsupported digit %q", r) + } + b.WriteByte('0' + v) + default: + return "", fmt.Errorf("invalid character %q", r) + } + } + return b.String(), nil +} + func lastIndexOfSubstring(_ environment, args ...rideType) (rideType, error) { s1, s2, err := twoStringsArgs(args) if err != nil { diff --git a/pkg/ride/functions_strings_test.go b/pkg/ride/functions_strings_test.go index cff00e9a67..4fb0c18699 100644 --- a/pkg/ride/functions_strings_test.go +++ b/pkg/ride/functions_strings_test.go @@ -415,7 +415,7 @@ func BenchmarkSplitString4C(b *testing.B) { } func TestParseInt(t *testing.T) { - for _, test := range []struct { + for i, test := range []struct { args []rideType fail bool r rideType @@ -427,19 +427,49 @@ func TestParseInt(t *testing.T) { {[]rideType{rideString("")}, false, rideUnit{}}, {[]rideType{rideString("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")}, false, rideUnit{}}, {[]rideType{rideString("abc")}, false, rideUnit{}}, + {[]rideType{rideString("⓪①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳")}, false, rideUnit{}}, + {[]rideType{rideString("ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ")}, false, rideUnit{}}, + {[]rideType{rideString("ⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹ")}, false, rideUnit{}}, + {[]rideType{rideString("⁰¹²³⁴⁵⁶⁷⁸⁹")}, false, rideUnit{}}, + {[]rideType{rideString("₀₁₂₃₄₅₆₇₈₉")}, false, rideUnit{}}, + {[]rideType{rideString("𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗")}, false, rideUnit{}}, + {[]rideType{rideString("𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡")}, false, rideUnit{}}, + {[]rideType{rideString("𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫")}, false, rideUnit{}}, + {[]rideType{rideString("𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵")}, false, rideUnit{}}, + {[]rideType{rideString("𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿")}, false, rideUnit{}}, + {[]rideType{rideString("0123456789")}, false, rideInt(123456789)}, + {[]rideType{rideString("٠١٢٣٤٥٦٧٨٩")}, false, rideInt(123456789)}, + {[]rideType{rideString("۰۱۲۳۴۵۶۷۸۹")}, false, rideInt(123456789)}, + {[]rideType{rideString("०१२३४५६७८९")}, false, rideInt(123456789)}, + {[]rideType{rideString("০১২৩৪৫৬৭৮৯")}, false, rideInt(123456789)}, + {[]rideType{rideString("๐๑๒๓๔๕๖๗๘๙")}, false, rideInt(123456789)}, + {[]rideType{rideString("௦௧௨௩௪௫௬௭௮௯")}, false, rideInt(123456789)}, + {[]rideType{rideString("၀၁၂၃၄၅၆၇၈၉")}, false, rideInt(123456789)}, + {[]rideType{rideString("០១២៣៤៥៦៧៨៩")}, false, rideInt(123456789)}, + {[]rideType{rideString("123")}, false, rideInt(123)}, + {[]rideType{rideString("100")}, false, rideInt(100)}, + {[]rideType{rideString("-123")}, false, rideInt(-123)}, + {[]rideType{rideString("+123")}, false, rideInt(123)}, + {[]rideType{rideString("0123456")}, false, rideInt(123456)}, + {[]rideType{rideString("12٣٤")}, false, rideInt(1234)}, + {[]rideType{rideString("١٢٣")}, false, rideInt(123)}, + {[]rideType{rideString("-١٢٣")}, false, rideInt(-123)}, + {[]rideType{rideString("-۱۲۳")}, false, rideInt(-123)}, {[]rideType{rideString("abc"), rideInt(0)}, true, nil}, {[]rideType{rideUnit{}}, true, nil}, {[]rideType{rideInt(1), rideString("x")}, true, nil}, {[]rideType{rideInt(1)}, true, nil}, {[]rideType{}, true, nil}, } { - r, err := parseInt(nil, test.args...) - if test.fail { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, test.r, r) - } + t.Run(fmt.Sprintf("test_%d", i+1), func(t *testing.T) { + r, err := parseInt(nil, test.args...) + if test.fail { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.r, r) + } + }) } } @@ -675,3 +705,23 @@ func TestContains(t *testing.T) { } } } + +func TestDigitsStringNormalization(t *testing.T) { + for i, test := range []struct { + s string + e string + }{ + {"", ""}, + {"123", "123"}, + {"123", "123"}, + {"-123", "-123"}, + {"+123", "+123"}, + {"0123456", "0123456"}, + } { + t.Run(fmt.Sprintf("test_%d", i+1), func(t *testing.T) { + n, err := normalizeDigits(test.s) + require.NoError(t, err) + require.Equal(t, test.e, n) + }) + } +} From a117a094c72c0679a4e9bad1764766d9c3a942a2 Mon Sep 17 00:00:00 2001 From: Alexey Kiselev Date: Mon, 18 May 2026 16:13:23 +0400 Subject: [PATCH 2/3] Review issues fixed. --- pkg/ride/functions_bigint.go | 4 +++- pkg/ride/functions_strings.go | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/ride/functions_bigint.go b/pkg/ride/functions_bigint.go index cd4a191fac..1bcf27c401 100644 --- a/pkg/ride/functions_bigint.go +++ b/pkg/ride/functions_bigint.go @@ -3,6 +3,7 @@ package ride import ( "math/big" "sort" + "unicode/utf8" "github.com/ericlagergren/decimal" "github.com/pkg/errors" @@ -518,7 +519,8 @@ func stringToBigInt(_ environment, args ...rideType) (rideType, error) { if err != nil { return nil, errors.Wrap(err, "stringToBigInt") } - if l := len(s); l > 155 { // 155 symbols is the length of math.MinBigInt value is string representation + const maxBitIntStringSize = 155 // The maximum allowed length of math.MinBigInt string. + if l := utf8.RuneCountInString(string(s)); l > maxBitIntStringSize { return nil, errors.Errorf("stringToBigInt: string is too long (%d symbols) for a BigInt", l) } ns, err := normalizeDigits(string(s)) diff --git a/pkg/ride/functions_strings.go b/pkg/ride/functions_strings.go index cc152bf348..39a37868bc 100644 --- a/pkg/ride/functions_strings.go +++ b/pkg/ride/functions_strings.go @@ -352,7 +352,7 @@ func digitValue(r rune) (byte, bool) { return 0, false } -// isMathematicalStyleDigit returns true for Mathematical Alphanumeric Symbols like 𝟎-𝟗, 𝟘-𝟡, 𝟢-𝟫, 𝟬-𝟵, 𝟶-𝟿. +// isMathematicalStyledDigit returns true for Mathematical Alphanumeric Symbols like 𝟎-𝟗, 𝟘-𝟡, 𝟢-𝟫, 𝟬-𝟵, 𝟶-𝟿. func isMathematicalStyledDigit(r rune) bool { return '\U0001D7CE' <= r && r <= '\U0001D7FF' } @@ -370,6 +370,12 @@ func normalizeDigits(s string) (string, error) { case '0' <= r && r <= '9': b.WriteRune(r) case unicode.IsDigit(r): + // Go's `unicode.IsDigit` reports more Unicode characters as digits than Java's implementation. + // Based on tests, mathematically styled digits are not treated as digits in the Scala node, + // so we filter them out here to preserve compatibility. + // + // This behavior may change in future versions of either platform, so compatibility should + // be rechecked when updating Go or Scala versions. if isMathematicalStyledDigit(r) { return "", fmt.Errorf("unsupported styled digit %q", r) } From 98392a1912c2dac358d2d135cd3f88e128980c32 Mon Sep 17 00:00:00 2001 From: Alexey Kiselev Date: Tue, 19 May 2026 17:45:37 +0400 Subject: [PATCH 3/3] Add logging of Eth transaction ID. --- pkg/proto/eth_transaction.go | 6 +++++- pkg/state/appender.go | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/proto/eth_transaction.go b/pkg/proto/eth_transaction.go index e33ed7c5d0..db5e962718 100644 --- a/pkg/proto/eth_transaction.go +++ b/pkg/proto/eth_transaction.go @@ -313,7 +313,11 @@ func (tx *EthereumTransaction) Verify() (*EthereumPublicKey, error) { signer := MakeEthereumSigner(tx.ChainId()) senderPK, err := signer.SenderPK(tx) if err != nil { - return nil, errors.Wrap(err, "failed to verify EthereumTransaction") + idStr := "n/a" + if tx.ID != nil { + idStr = tx.ID.String() + } + return nil, errors.Wrapf(err, "failed to verify EthereumTransaction '%s'", idStr) } tx.threadSafeSetSenderPK(senderPK) return senderPK, nil diff --git a/pkg/state/appender.go b/pkg/state/appender.go index 0d864fb4d7..a2340b31fb 100644 --- a/pkg/state/appender.go +++ b/pkg/state/appender.go @@ -6,7 +6,7 @@ import ( "log/slog" "github.com/ccoveille/go-safecast/v2" - "github.com/mr-tron/base58/base58" + "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/wavesplatform/gowaves/pkg/crypto" @@ -716,7 +716,9 @@ func (a *txAppender) appendTxs( txSnap, errAppendTx := a.appendTx(tx, appendTxArgs) if errAppendTx != nil { // TODO: check error type for elided tx if !isBlockWithChallenge { - return proto.BlockSnapshot{}, crypto.Digest{}, errAppendTx + return proto.BlockSnapshot{}, crypto.Digest{}, + errors.Wrapf(errAppendTx, "failed to append tx %q at height %d", base58.Encode(txID), + blockInfo.Height) } slog.Debug("Elided tx detected", slog.String("ID", base58.Encode(txID)), logging.Error(errAppendTx)) txSnap = txSnapshot{