diff --git a/.golangci.yml b/.golangci.yml index e9714ba5811..6c65f9d8556 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,6 +11,7 @@ run: - ".*\\.pb\\.gw\\.go$" - ".*\\.pulsar\\.go$" - crypto/keys/secp256k1/internal/* + - types/coin_regex.go build-tags: - e2e diff --git a/CHANGELOG.md b/CHANGELOG.md index ef4a79ce494..7b610eb219f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Every module contains its own CHANGELOG.md. Please refer to the module you are i ### Features +* (types) [#19511](https://github.com/cosmos/cosmos-sdk/pull/19511) Replace regex parsing of denom validation to direct matching methods. * (runtime) [#19571](https://github.com/cosmos/cosmos-sdk/pull/19571) Implement `core/router.Service` it in runtime. This service is present in all modules (when using depinject). * (types) [#19164](https://github.com/cosmos/cosmos-sdk/pull/19164) Add a ValueCodec for the math.Uint type that can be used in collections maps. * (types) [#19281](https://github.com/cosmos/cosmos-sdk/pull/19281) Added a new method, `IsGT`, for `types.Coin`. This method is used to check if a `types.Coin` is greater than another `types.Coin`. diff --git a/tests/integration/bank/keeper/deterministic_test.go b/tests/integration/bank/keeper/deterministic_test.go index 54d51b0e20b..635e061dd91 100644 --- a/tests/integration/bank/keeper/deterministic_test.go +++ b/tests/integration/bank/keeper/deterministic_test.go @@ -33,7 +33,7 @@ import ( ) var ( - denomRegex = sdk.DefaultCoinDenomRegex() + denomRegex = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}` addr1 = sdk.MustAccAddressFromBech32("cosmos139f7kncmglres2nf3h4hc4tade85ekfr8sulz5") coin1 = sdk.NewCoin("denom", math.NewInt(10)) metadataAtom = banktypes.Metadata{ diff --git a/tests/integration/staking/keeper/deterministic_test.go b/tests/integration/staking/keeper/deterministic_test.go index 961059c272f..cc8c2fbae65 100644 --- a/tests/integration/staking/keeper/deterministic_test.go +++ b/tests/integration/staking/keeper/deterministic_test.go @@ -859,9 +859,10 @@ func TestGRPCRedelegations(t *testing.T) { func TestGRPCParams(t *testing.T) { t.Parallel() f := initDeterministicFixture(t) + coinDenomRegex := `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}` rapid.Check(t, func(rt *rapid.T) { - bondDenom := rapid.StringMatching(sdk.DefaultCoinDenomRegex()).Draw(rt, "bond-denom") + bondDenom := rapid.StringMatching(coinDenomRegex).Draw(rt, "bond-denom") params := stakingtypes.Params{ BondDenom: bondDenom, UnbondingTime: durationGenerator().Draw(rt, "duration"), diff --git a/types/coin.go b/types/coin.go index 6df1c26e4ef..3f32bc34975 100644 --- a/types/coin.go +++ b/types/coin.go @@ -842,30 +842,15 @@ func (coins Coins) Sort() Coins { return coins } -//----------------------------------------------------------------------------- -// Parsing - var ( - // Denominations can be 3 ~ 128 characters long and support letters, followed by either - // a letter, a number or a separator ('/', ':', '.', '_' or '-'). - reDnmString = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}` - reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+` - reSpc = `[[:space:]]*` - reDnm *regexp.Regexp - reDecCoin *regexp.Regexp -) - -func init() { - SetCoinDenomRegex(DefaultCoinDenomRegex) -} + reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+` + reSpc = `[[:space:]]*` -// DefaultCoinDenomRegex returns the default regex string -func DefaultCoinDenomRegex() string { - return reDnmString -} + coinDenomRegex func() string -// coinDenomRegex returns the current regex string and can be overwritten for custom validation -var coinDenomRegex = DefaultCoinDenomRegex + reDnm *regexp.Regexp + reDecCoin *regexp.Regexp +) // SetCoinDenomRegex allows for coin's custom validation by overriding the regular // expression string used for denom validation. @@ -878,9 +863,18 @@ func SetCoinDenomRegex(reFn func() string) { // ValidateDenom is the default validation function for Coin.Denom. func ValidateDenom(denom string) error { - if !reDnm.MatchString(denom) { + if reDnm == nil || reDecCoin == nil { + // Convert the string to a byte slice as required by the Ragel-generated function. + denomBytes := []byte(denom) + + // Call the Ragel-generated function. + if !MatchDenom(denomBytes) { + return fmt.Errorf("invalid denom: %s", denom) + } + } else if !reDnm.MatchString(denom) { // If reDnm has been initialized, use it for matching. return fmt.Errorf("invalid denom: %s", denom) } + return nil } diff --git a/types/coin_regex.go b/types/coin_regex.go new file mode 100644 index 00000000000..ea6134a9b82 --- /dev/null +++ b/types/coin_regex.go @@ -0,0 +1,173 @@ +//line coin_regex.rl:1 +// `coin_regex.go` is generated by regel using `ragel -Z coin_regex.rl`. +// do not directly edit `coin_regex.go`. +// source: types/coin_regex.rl +// nolint:gocritic,unused,ineffassign + +// Regex parsing of denoms were as the following +// reDnmString = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}` +// reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+` +// reSpc = `[[:space:]]*` + +// reDnm = regexp.MustCompile(fmt.Sprintf(`^%s$`, coinDenomRegex())) +// reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, coinDenomRegex())) + +package types + +func MatchDenom(data []byte) bool { + var _scanner_actions []byte = []byte{ + 0, 1, 0, + } + + var _scanner_key_offsets []byte = []byte{ + 0, 0, 4, 11, + } + + var _scanner_trans_keys []byte = []byte{ + 65, 90, 97, 122, 95, 45, 58, 65, + 90, 97, 122, + } + + var _scanner_single_lengths []byte = []byte{ + 0, 0, 1, 0, + } + + var _scanner_range_lengths []byte = []byte{ + 0, 2, 3, 0, + } + + var _scanner_index_offsets []byte = []byte{ + 0, 0, 3, 8, + } + + var _scanner_indicies []byte = []byte{ + 0, 0, 1, 2, 2, 2, 2, 1, + 1, + } + + var _scanner_trans_targs []byte = []byte{ + 2, 0, 3, + } + + var _scanner_trans_actions []byte = []byte{ + 0, 0, 1, + } + + const scanner_start int = 1 + const scanner_first_final int = 3 + const scanner_error int = 0 + + const scanner_en_main int = 1 + + if len(data) < 3 || len(data) > 128 { + return false + } + cs, p, pe, eof := 0, 0, len(data), len(data) + _ = eof + + { + cs = scanner_start + } + + { + var _klen int + var _trans int + var _acts int + var _nacts uint + var _keys int + if p == pe { + goto _test_eof + } + if cs == 0 { + goto _out + } + _resume: + _keys = int(_scanner_key_offsets[cs]) + _trans = int(_scanner_index_offsets[cs]) + + _klen = int(_scanner_single_lengths[cs]) + if _klen > 0 { + _lower := int(_keys) + var _mid int + _upper := int(_keys + _klen - 1) + for { + if _upper < _lower { + break + } + + _mid = _lower + ((_upper - _lower) >> 1) + switch { + case data[p] < _scanner_trans_keys[_mid]: + _upper = _mid - 1 + case data[p] > _scanner_trans_keys[_mid]: + _lower = _mid + 1 + default: + _trans += int(_mid - int(_keys)) + goto _match + } + } + _keys += _klen + _trans += _klen + } + + _klen = int(_scanner_range_lengths[cs]) + if _klen > 0 { + _lower := int(_keys) + var _mid int + _upper := int(_keys + (_klen << 1) - 2) + for { + if _upper < _lower { + break + } + + _mid = _lower + (((_upper - _lower) >> 1) & ^1) + switch { + case data[p] < _scanner_trans_keys[_mid]: + _upper = _mid - 2 + case data[p] > _scanner_trans_keys[_mid+1]: + _lower = _mid + 2 + default: + _trans += int((_mid - int(_keys)) >> 1) + goto _match + } + } + _trans += _klen + } + + _match: + _trans = int(_scanner_indicies[_trans]) + cs = int(_scanner_trans_targs[_trans]) + + if _scanner_trans_actions[_trans] == 0 { + goto _again + } + + _acts = int(_scanner_trans_actions[_trans]) + _nacts = uint(_scanner_actions[_acts]) + _acts++ + for ; _nacts > 0; _nacts-- { + _acts++ + switch _scanner_actions[_acts-1] { + case 0: + return true + } + } + + _again: + if cs == 0 { + goto _out + } + p++ + if p != pe { + goto _resume + } + _test_eof: + { + } + _out: + { + } + } + + return false +} diff --git a/types/coin_regex.rl b/types/coin_regex.rl new file mode 100644 index 00000000000..4a4355162b8 --- /dev/null +++ b/types/coin_regex.rl @@ -0,0 +1,40 @@ +// `coin_regex.go` is generated by regel using `ragel -Z coin_regex.rl`. +// do not directly edit `coin_regex.go`. +// source: types/coin_regex.rl +// nolint:gocritic,unused,ineffassign + + +// Regex parsing of denoms were as the following +// reDnmString = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}` +// reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+` +// reSpc = `[[:space:]]*` + +// reDnm = regexp.MustCompile(fmt.Sprintf(`^%s$`, coinDenomRegex())) +// reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, coinDenomRegex())) + +package types + +func MatchDenom(data []byte) bool { +%% machine scanner; +%% write data; + + if len(data) < 3 || len(data) > 128 { + return false + } + cs, p, pe, eof := 0, 0, len(data), len(data) + _ = eof + %%{ + # Define character classes + special = '/' | ':' | '.' | '_' | '-'; + + denom_pattern = [a-zA-Z] (alnum | special); + + + # Combined pattern for matching either a denomination or a decimal amount + main := denom_pattern @{ return true }; + + write init; + write exec; + }%% + return false +} \ No newline at end of file diff --git a/types/coin_test.go b/types/coin_test.go index 734af669e1c..e805a407e16 100644 --- a/types/coin_test.go +++ b/types/coin_test.go @@ -108,6 +108,8 @@ func (s *coinTestSuite) TestCoinIsValid() { func (s *coinTestSuite) TestCustomValidation() { newDnmRegex := `[\x{1F600}-\x{1F6FF}]` + reDnmString := `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}` + sdk.SetCoinDenomRegex(func() string { return newDnmRegex }) @@ -126,7 +128,7 @@ func (s *coinTestSuite) TestCustomValidation() { for i, tc := range cases { s.Require().Equal(tc.expectPass, tc.coin.IsValid(), "unexpected result for IsValid, tc #%d", i) } - sdk.SetCoinDenomRegex(sdk.DefaultCoinDenomRegex) + sdk.SetCoinDenomRegex(func() string { return reDnmString }) } func (s *coinTestSuite) TestCoinsDenoms() { diff --git a/types/dec_coin.go b/types/dec_coin.go index c1bbcc6a8ec..37bcd5b633a 100644 --- a/types/dec_coin.go +++ b/types/dec_coin.go @@ -4,6 +4,7 @@ import ( "fmt" "sort" "strings" + "unicode" "cosmossdk.io/errors" "cosmossdk.io/math" @@ -630,14 +631,23 @@ func (coins DecCoins) Sort() DecCoins { // ParseDecCoin parses a decimal coin from a string, returning an error if // invalid. An empty string is considered invalid. func ParseDecCoin(coinStr string) (coin DecCoin, err error) { - coinStr = strings.TrimSpace(coinStr) + var amountStr, denomStr string + // if custom parsing has not been set, use default coin regex + if reDecCoin == nil { + amountStr, denomStr, err = ParseDecAmount(coinStr) + if err != nil { + return DecCoin{}, err + } + } else { + coinStr = strings.TrimSpace(coinStr) - matches := reDecCoin.FindStringSubmatch(coinStr) - if matches == nil { - return DecCoin{}, fmt.Errorf("invalid decimal coin expression: %s", coinStr) - } + matches := reDecCoin.FindStringSubmatch(coinStr) + if matches == nil { + return DecCoin{}, fmt.Errorf("invalid decimal coin expression: %s", coinStr) + } - amountStr, denomStr := matches[1], matches[2] + amountStr, denomStr = matches[1], matches[2] + } amount, err := math.LegacyNewDecFromStr(amountStr) if err != nil { @@ -651,6 +661,50 @@ func ParseDecCoin(coinStr string) (coin DecCoin, err error) { return NewDecCoinFromDec(denomStr, amount), nil } +// ParseDecAmount parses the given string into amount, denomination. +func ParseDecAmount(coinStr string) (string, string, error) { + var amountRune, denomRune []rune + + // Indicates the start of denom parsing + seenLetter := false + // Indicates we're currently parsing the amount + parsingAmount := true + + for _, r := range strings.TrimSpace(coinStr) { + if parsingAmount { + if unicode.IsDigit(r) || r == '.' { + amountRune = append(amountRune, r) + } else if unicode.IsSpace(r) { // if space is seen, indicates that we have finished parsing amount + parsingAmount = false + } else if unicode.IsLetter(r) { // if letter is seen, indicates that it is the start of denom + parsingAmount = false + seenLetter = true + denomRune = append(denomRune, r) + } else { // Invalid character encountered in amount part + return "", "", fmt.Errorf("invalid character in coin string: %s", string(r)) + } + } else if !seenLetter { // This logic flow is for skipping spaces between amount and denomination + if unicode.IsLetter(r) { + seenLetter = true + denomRune = append(denomRune, r) + } else if !unicode.IsSpace(r) { + // Invalid character before denomination starts + return "", "", fmt.Errorf("invalid start of denomination: %s", string(r)) + } + } else { + // Parsing the denomination + if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '/' || r == ':' || r == '.' || r == '_' || r == '-' { + denomRune = append(denomRune, r) + } else { + // Invalid character encountered in denomination part + return "", "", fmt.Errorf("invalid character in denomination: %s", string(r)) + } + } + } + + return string(amountRune), string(denomRune), nil +} + // ParseDecCoins will parse out a list of decimal coins separated by commas. If the parsing is successuful, // the provided coins will be sanitized by removing zero coins and sorting the coin set. Lastly // a validation of the coin set is executed. If the check passes, ParseDecCoins will return the sanitized coins. diff --git a/types/dec_coin_test.go b/types/dec_coin_test.go index 915a803bd08..8ae3f507909 100644 --- a/types/dec_coin_test.go +++ b/types/dec_coin_test.go @@ -377,6 +377,9 @@ func (s *decCoinTestSuite) TestParseDecCoins() { }{ {"", nil, false}, {"4stake", sdk.DecCoins{sdk.NewDecCoinFromDec("stake", math.LegacyNewDecFromInt(math.NewInt(4)))}, false}, + {"5.5atom", sdk.DecCoins{ + sdk.NewDecCoinFromDec("atom", math.LegacyNewDecWithPrec(5500000000000000000, math.LegacyPrecision)), + }, false}, {"5.5atom,4stake", sdk.DecCoins{ sdk.NewDecCoinFromDec("atom", math.LegacyNewDecWithPrec(5500000000000000000, math.LegacyPrecision)), sdk.NewDecCoinFromDec("stake", math.LegacyNewDec(4)),