Skip to content

Commit

Permalink
feat(x/bank): Replace regex parsing of denom validation to generated …
Browse files Browse the repository at this point in the history
…parsing (cosmos#19511)
  • Loading branch information
mattverse committed Mar 7, 2024
1 parent 4edf6b2 commit a1e3a85
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 31 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ run:
- ".*\\.pb\\.gw\\.go$"
- ".*\\.pulsar\\.go$"
- crypto/keys/secp256k1/internal/*
- types/coin_regex.go

build-tags:
- e2e
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/bank/keeper/deterministic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/staking/keeper/deterministic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
38 changes: 16 additions & 22 deletions types/coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

Expand Down
173 changes: 173 additions & 0 deletions types/coin_regex.go
Original file line number Diff line number Diff line change
@@ -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 changes: 40 additions & 0 deletions types/coin_regex.rl
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 3 additions & 1 deletion types/coin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand All @@ -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() {
Expand Down
Loading

0 comments on commit a1e3a85

Please sign in to comment.