feat(x/bank): Replace regex parsing of denom validation to generated parsing (#19511)

This commit is contained in:
Matt, Park 2024-03-07 18:34:49 +09:00 committed by GitHub
parent 4edf6b2a85
commit a1e3a85ae1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 301 additions and 32 deletions

View File

@ -11,6 +11,7 @@ run:
- ".*\\.pb\\.gw\\.go$"
- ".*\\.pulsar\\.go$"
- crypto/keys/secp256k1/internal/*
- types/coin_regex.go
build-tags:
- e2e

View File

@ -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`.

View File

@ -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{

View File

@ -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"),

View File

@ -842,31 +842,16 @@ 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
reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+`
reSpc = `[[:space:]]*`
coinDenomRegex func() string
reDnm *regexp.Regexp
reDecCoin *regexp.Regexp
)
func init() {
SetCoinDenomRegex(DefaultCoinDenomRegex)
}
// DefaultCoinDenomRegex returns the default regex string
func DefaultCoinDenomRegex() string {
return reDnmString
}
// coinDenomRegex returns the current regex string and can be overwritten for custom validation
var coinDenomRegex = DefaultCoinDenomRegex
// SetCoinDenomRegex allows for coin's custom validation by overriding the regular
// expression string used for denom validation.
func SetCoinDenomRegex(reFn func() string) {
@ -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
}

173
types/coin_regex.go Normal file
View File

@ -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
}

40
types/coin_regex.rl Normal file
View File

@ -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
}

View File

@ -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() {

View File

@ -4,6 +4,7 @@ import (
"fmt"
"sort"
"strings"
"unicode"
"cosmossdk.io/errors"
"cosmossdk.io/math"
@ -630,15 +631,24 @@ 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 {
return DecCoin{}, errors.Wrap(err, fmt.Sprintf("failed to parse decimal coin amount: %s", amountStr))
@ -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.

View File

@ -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)),