From e9dd2d8b9a1af0a86295deab5c2c27b77b990e0e Mon Sep 17 00:00:00 2001 From: "Matt, Park" <45252226+mattverse@users.noreply.github.com> Date: Thu, 7 Mar 2024 18:34:49 +0900 Subject: [PATCH] feat(x/bank): Replace regex parsing of denom validation to generated parsing (#19511) --- .golangci.yml | 1 + .../bank/keeper/deterministic_test.go | 2 +- .../staking/keeper/deterministic_test.go | 896 ++++++++++++++++++ types/coin.go | 38 +- types/coin_regex.go | 173 ++++ types/coin_regex.rl | 40 + types/coin_test.go | 4 +- types/dec_coin.go | 66 +- types/dec_coin_test.go | 3 + 9 files changed, 1193 insertions(+), 30 deletions(-) create mode 100644 tests/integration/staking/keeper/deterministic_test.go create mode 100644 types/coin_regex.go create mode 100644 types/coin_regex.rl diff --git a/.golangci.yml b/.golangci.yml index f6db509d83cd..a22f35a5db7c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,6 +4,7 @@ run: timeout: 5m skip-files: - server/grpc/gogoreflection/serverreflection.go + - types/coin_regex.go linters: disable-all: true diff --git a/tests/integration/bank/keeper/deterministic_test.go b/tests/integration/bank/keeper/deterministic_test.go index 112d11489260..1bc3d4de8cb5 100644 --- a/tests/integration/bank/keeper/deterministic_test.go +++ b/tests/integration/bank/keeper/deterministic_test.go @@ -36,7 +36,7 @@ type DeterministicTestSuite struct { } var ( - denomRegex = sdk.DefaultCoinDenomRegex() + denomRegex = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}` addr1 = sdk.MustAccAddressFromBech32("cosmos139f7kncmglres2nf3h4hc4tade85ekfr8sulz5") coin1 = sdk.NewCoin("denom", sdk.NewInt(10)) metadataAtom = banktypes.Metadata{ diff --git a/tests/integration/staking/keeper/deterministic_test.go b/tests/integration/staking/keeper/deterministic_test.go new file mode 100644 index 000000000000..cc8c2fbae654 --- /dev/null +++ b/tests/integration/staking/keeper/deterministic_test.go @@ -0,0 +1,896 @@ +package keeper_test + +import ( + "testing" + "time" + + "gotest.tools/v3/assert" + "pgregory.net/rapid" + + "cosmossdk.io/core/appmodule" + "cosmossdk.io/log" + "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + "cosmossdk.io/x/auth" + authkeeper "cosmossdk.io/x/auth/keeper" + authsims "cosmossdk.io/x/auth/simulation" + authtypes "cosmossdk.io/x/auth/types" + "cosmossdk.io/x/bank" + bankkeeper "cosmossdk.io/x/bank/keeper" + banktestutil "cosmossdk.io/x/bank/testutil" + banktypes "cosmossdk.io/x/bank/types" + "cosmossdk.io/x/distribution" + minttypes "cosmossdk.io/x/mint/types" + "cosmossdk.io/x/staking" + stakingkeeper "cosmossdk.io/x/staking/keeper" + stakingtypes "cosmossdk.io/x/staking/types" + + "github.com/cosmos/cosmos-sdk/codec" + addresscodec "github.com/cosmos/cosmos-sdk/codec/address" + codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/testutil/integration" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" +) + +var ( + validator1 = "cosmosvaloper1qqqryrs09ggeuqszqygqyqd2tgqmsqzewacjj7" + validatorAddr1, _ = sdk.ValAddressFromBech32(validator1) + validator2 = "cosmosvaloper1gghjut3ccd8ay0zduzj64hwre2fxs9ldmqhffj" + validatorAddr2, _ = sdk.ValAddressFromBech32(validator2) + delegator1 = "cosmos1nph3cfzk6trsmfxkeu943nvach5qw4vwstnvkl" + delegatorAddr1 = sdk.MustAccAddressFromBech32(delegator1) + delegator2 = "cosmos139f7kncmglres2nf3h4hc4tade85ekfr8sulz5" + delegatorAddr2 = sdk.MustAccAddressFromBech32(delegator2) +) + +type deterministicFixture struct { + app *integration.App + + ctx sdk.Context + cdc codec.Codec + keys map[string]*storetypes.KVStoreKey + + accountKeeper authkeeper.AccountKeeper + bankKeeper bankkeeper.BaseKeeper + stakingKeeper *stakingkeeper.Keeper + + queryClient stakingtypes.QueryClient + amt1 math.Int + amt2 math.Int +} + +func initDeterministicFixture(t *testing.T) *deterministicFixture { + t.Helper() + keys := storetypes.NewKVStoreKeys( + authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, + ) + encodingCfg := moduletestutil.MakeTestEncodingConfig(codectestutil.CodecOptions{}, auth.AppModule{}, distribution.AppModule{}) + cdc := encodingCfg.Codec + + logger := log.NewTestLogger(t) + cms := integration.CreateMultiStore(keys, logger) + + newCtx := sdk.NewContext(cms, true, logger) + + authority := authtypes.NewModuleAddress("gov") + + maccPerms := map[string][]string{ + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + } + + accountKeeper := authkeeper.NewAccountKeeper( + runtime.NewEnvironment(runtime.NewKVStoreService(keys[authtypes.StoreKey]), log.NewNopLogger()), + cdc, + authtypes.ProtoBaseAccount, + maccPerms, + addresscodec.NewBech32Codec(sdk.Bech32MainPrefix), + sdk.Bech32MainPrefix, + authority.String(), + ) + + blockedAddresses := map[string]bool{ + accountKeeper.GetAuthority(): false, + } + bankKeeper := bankkeeper.NewBaseKeeper( + runtime.NewEnvironment(runtime.NewKVStoreService(keys[banktypes.StoreKey]), log.NewNopLogger()), + cdc, + accountKeeper, + blockedAddresses, + authority.String(), + ) + + stakingKeeper := stakingkeeper.NewKeeper(cdc, runtime.NewEnvironment(runtime.NewKVStoreService(keys[stakingtypes.StoreKey]), log.NewNopLogger()), accountKeeper, bankKeeper, authority.String(), addresscodec.NewBech32Codec(sdk.Bech32PrefixValAddr), addresscodec.NewBech32Codec(sdk.Bech32PrefixConsAddr)) + + authModule := auth.NewAppModule(cdc, accountKeeper, authsims.RandomGenesisAccounts) + bankModule := bank.NewAppModule(cdc, bankKeeper, accountKeeper) + stakingModule := staking.NewAppModule(cdc, stakingKeeper, accountKeeper, bankKeeper) + + integrationApp := integration.NewIntegrationApp(newCtx, logger, keys, cdc, + encodingCfg.InterfaceRegistry.SigningContext().AddressCodec(), + encodingCfg.InterfaceRegistry.SigningContext().ValidatorAddressCodec(), + map[string]appmodule.AppModule{ + authtypes.ModuleName: authModule, + banktypes.ModuleName: bankModule, + stakingtypes.ModuleName: stakingModule, + }) + + ctx := integrationApp.Context() + + // Register MsgServer and QueryServer + stakingtypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), stakingkeeper.NewMsgServerImpl(stakingKeeper)) + stakingtypes.RegisterQueryServer(integrationApp.QueryHelper(), stakingkeeper.NewQuerier(stakingKeeper)) + + // set default staking params + assert.NilError(t, stakingKeeper.Params.Set(ctx, stakingtypes.DefaultParams())) + + // set pools + startTokens := stakingKeeper.TokensFromConsensusPower(ctx, 10) + bondDenom, err := stakingKeeper.BondDenom(ctx) + assert.NilError(t, err) + notBondedPool := stakingKeeper.GetNotBondedPool(ctx) + assert.NilError(t, banktestutil.FundModuleAccount(ctx, bankKeeper, notBondedPool.GetName(), sdk.NewCoins(sdk.NewCoin(bondDenom, startTokens)))) + accountKeeper.SetModuleAccount(ctx, notBondedPool) + bondedPool := stakingKeeper.GetBondedPool(ctx) + assert.NilError(t, banktestutil.FundModuleAccount(ctx, bankKeeper, bondedPool.GetName(), sdk.NewCoins(sdk.NewCoin(bondDenom, startTokens)))) + accountKeeper.SetModuleAccount(ctx, bondedPool) + + qr := integrationApp.QueryHelper() + queryClient := stakingtypes.NewQueryClient(qr) + + amt1 := stakingKeeper.TokensFromConsensusPower(ctx, 101) + amt2 := stakingKeeper.TokensFromConsensusPower(ctx, 102) + + f := deterministicFixture{ + app: integrationApp, + ctx: sdk.UnwrapSDKContext(ctx), + cdc: cdc, + keys: keys, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + stakingKeeper: stakingKeeper, + queryClient: queryClient, + amt1: amt1, + amt2: amt2, + } + + return &f +} + +func durationGenerator() *rapid.Generator[time.Duration] { + return rapid.Custom(func(t *rapid.T) time.Duration { + now := time.Now() + // range from current time to 365days. + duration := rapid.Int64Range(now.Unix(), 365*24*60*60*now.Unix()).Draw(t, "time") + return time.Duration(duration) + }) +} + +func pubKeyGenerator() *rapid.Generator[ed25519.PubKey] { + return rapid.Custom(func(t *rapid.T) ed25519.PubKey { + pkBz := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "hex") + return ed25519.PubKey{Key: pkBz} + }) +} + +func bondTypeGenerator() *rapid.Generator[stakingtypes.BondStatus] { + bondTypes := []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Unbonded, stakingtypes.Unbonding} + return rapid.Custom(func(t *rapid.T) stakingtypes.BondStatus { + return bondTypes[rapid.IntRange(0, 2).Draw(t, "range")] + }) +} + +// createValidator creates a validator with random values. +func createValidator(t *testing.T, rt *rapid.T, f *deterministicFixture) stakingtypes.Validator { + t.Helper() + pubkey := pubKeyGenerator().Draw(rt, "pubkey") + pubkeyAny, err := codectypes.NewAnyWithValue(&pubkey) + assert.NilError(t, err) + return stakingtypes.Validator{ + OperatorAddress: sdk.ValAddress(testdata.AddressGenerator(rt).Draw(rt, "address")).String(), + ConsensusPubkey: pubkeyAny, + Jailed: rapid.Bool().Draw(rt, "jailed"), + Status: bondTypeGenerator().Draw(rt, "bond-status"), + Tokens: math.NewInt(rapid.Int64Min(10000).Draw(rt, "tokens")), + DelegatorShares: math.LegacyNewDecWithPrec(rapid.Int64Range(1, 100).Draw(rt, "commission"), 2), + Description: stakingtypes.NewDescription( + rapid.StringN(5, 250, 255).Draw(rt, "moniker"), + rapid.StringN(5, 250, 255).Draw(rt, "identity"), + rapid.StringN(5, 250, 255).Draw(rt, "website"), + rapid.StringN(5, 250, 255).Draw(rt, "securityContact"), + rapid.StringN(5, 250, 255).Draw(rt, "details"), + ), + UnbondingHeight: rapid.Int64Min(1).Draw(rt, "unbonding-height"), + UnbondingTime: time.Now().Add(durationGenerator().Draw(rt, "duration")), + Commission: stakingtypes.NewCommission( + math.LegacyNewDecWithPrec(rapid.Int64Range(0, 100).Draw(rt, "rate"), 2), + math.LegacyNewDecWithPrec(rapid.Int64Range(0, 100).Draw(rt, "max-rate"), 2), + math.LegacyNewDecWithPrec(rapid.Int64Range(0, 100).Draw(rt, "max-change-rate"), 2), + ), + MinSelfDelegation: math.NewInt(rapid.Int64Min(1).Draw(rt, "tokens")), + } +} + +// createAndSetValidatorWithStatus creates a validator with random values but with given status and sets to the state +func createAndSetValidatorWithStatus(t *testing.T, rt *rapid.T, f *deterministicFixture, status stakingtypes.BondStatus) stakingtypes.Validator { + t.Helper() + val := createValidator(t, rt, f) + val.Status = status + setValidator(t, f, val) + return val +} + +// createAndSetValidator creates a validator with random values and sets to the state +func createAndSetValidator(t *testing.T, rt *rapid.T, f *deterministicFixture) stakingtypes.Validator { + t.Helper() + val := createValidator(t, rt, f) + setValidator(t, f, val) + return val +} + +func setValidator(t *testing.T, f *deterministicFixture, validator stakingtypes.Validator) { + t.Helper() + assert.NilError(t, f.stakingKeeper.SetValidator(f.ctx, validator)) + assert.NilError(t, f.stakingKeeper.SetValidatorByPowerIndex(f.ctx, validator)) + assert.NilError(t, f.stakingKeeper.SetValidatorByConsAddr(f.ctx, validator)) + valbz, err := f.stakingKeeper.ValidatorAddressCodec().StringToBytes(validator.GetOperator()) + assert.NilError(t, err) + + assert.NilError(t, f.stakingKeeper.Hooks().AfterValidatorCreated(f.ctx, valbz)) + + delegatorAddress := sdk.AccAddress(valbz) + coins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, validator.BondedTokens())) + assert.NilError(t, banktestutil.FundAccount(f.ctx, f.bankKeeper, delegatorAddress, coins)) + + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddress) + f.accountKeeper.SetAccount(f.ctx, acc) + + _, err = f.stakingKeeper.Delegate(f.ctx, delegatorAddress, validator.BondedTokens(), stakingtypes.Unbonded, validator, true) + assert.NilError(t, err) +} + +// getStaticValidator creates a validator with hard-coded values and sets to the state. +func getStaticValidator(t *testing.T, f *deterministicFixture) stakingtypes.Validator { + t.Helper() + pubkey := ed25519.PubKey{Key: []byte{24, 179, 242, 2, 151, 3, 34, 6, 1, 11, 0, 194, 202, 201, 77, 1, 167, 40, 249, 115, 32, 97, 18, 1, 1, 127, 255, 103, 13, 1, 34, 1}} + pubkeyAny, err := codectypes.NewAnyWithValue(&pubkey) + assert.NilError(t, err) + + validator := stakingtypes.Validator{ + OperatorAddress: validator1, + ConsensusPubkey: pubkeyAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(100), + DelegatorShares: math.LegacyNewDecWithPrec(5, 2), + Description: stakingtypes.NewDescription( + "moniker", + "identity", + "website", + "securityContact", + "details", + ), + UnbondingHeight: 10, + UnbondingTime: time.Date(2022, 10, 1, 0, 0, 0, 0, time.UTC), + Commission: stakingtypes.NewCommission( + math.LegacyNewDecWithPrec(5, 2), + math.LegacyNewDecWithPrec(5, 2), + math.LegacyNewDecWithPrec(5, 2), + ), + MinSelfDelegation: math.NewInt(10), + } + + setValidator(t, f, validator) + return validator +} + +// getStaticValidator2 creates a validator with hard-coded values and sets to the state. +func getStaticValidator2(t *testing.T, f *deterministicFixture) stakingtypes.Validator { + t.Helper() + pubkey := ed25519.PubKey{Key: []byte{40, 249, 115, 32, 97, 18, 1, 1, 127, 255, 103, 13, 1, 34, 1, 24, 179, 242, 2, 151, 3, 34, 6, 1, 11, 0, 194, 202, 201, 77, 1, 167}} + pubkeyAny, err := codectypes.NewAnyWithValue(&pubkey) + assert.NilError(t, err) + + validator := stakingtypes.Validator{ + OperatorAddress: validator2, + ConsensusPubkey: pubkeyAny, + Jailed: true, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(10012), + DelegatorShares: math.LegacyNewDecWithPrec(96, 2), + Description: stakingtypes.NewDescription( + "moniker", + "identity", + "website", + "securityContact", + "details", + ), + UnbondingHeight: 100132, + UnbondingTime: time.Date(2025, 10, 1, 0, 0, 0, 0, time.UTC), + Commission: stakingtypes.NewCommission( + math.LegacyNewDecWithPrec(15, 2), + math.LegacyNewDecWithPrec(59, 2), + math.LegacyNewDecWithPrec(51, 2), + ), + MinSelfDelegation: math.NewInt(1), + } + setValidator(t, f, validator) + + return validator +} + +// createDelegationAndDelegate funds the delegator account with a random delegation in range 100-1000 and delegates. +func createDelegationAndDelegate(t *testing.T, rt *rapid.T, f *deterministicFixture, delegator sdk.AccAddress, validator stakingtypes.Validator) (newShares math.LegacyDec, err error) { + t.Helper() + amt := f.stakingKeeper.TokensFromConsensusPower(f.ctx, rapid.Int64Range(100, 1000).Draw(rt, "amount")) + return fundAccountAndDelegate(t, f, delegator, validator, amt) +} + +// fundAccountAndDelegate funds the delegator account with the specified delegation and delegates. +func fundAccountAndDelegate(t *testing.T, f *deterministicFixture, delegator sdk.AccAddress, validator stakingtypes.Validator, amt math.Int) (newShares math.LegacyDec, err error) { + t.Helper() + coins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, amt)) + + assert.NilError(t, f.bankKeeper.MintCoins(f.ctx, minttypes.ModuleName, coins)) + assert.NilError(t, banktestutil.FundAccount(f.ctx, f.bankKeeper, delegator, coins)) + + shares, err := f.stakingKeeper.Delegate(f.ctx, delegator, amt, stakingtypes.Unbonded, validator, true) + return shares, err +} + +func TestGRPCValidator(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + val := createAndSetValidator(t, rt, f) + req := &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: val.OperatorAddress, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.Validator, 0, true) + }) + + f = initDeterministicFixture(t) // reset + val := getStaticValidator(t, f) + req := &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: val.OperatorAddress, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.Validator, 1915, false) +} + +func TestGRPCValidators(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + validatorStatus := []string{stakingtypes.BondStatusBonded, stakingtypes.BondStatusUnbonded, stakingtypes.BondStatusUnbonding} + rapid.Check(t, func(rt *rapid.T) { + valsCount := rapid.IntRange(1, 3).Draw(rt, "num-validators") + for i := 0; i < valsCount; i++ { + createAndSetValidator(t, rt, f) + } + + req := &stakingtypes.QueryValidatorsRequest{ + Status: validatorStatus[rapid.IntRange(0, 2).Draw(rt, "status")], + Pagination: testdata.PaginationGenerator(rt, uint64(valsCount)).Draw(rt, "pagination"), + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.Validators, 0, true) + }) + + f = initDeterministicFixture(t) // reset + getStaticValidator(t, f) + getStaticValidator2(t, f) + + testdata.DeterministicIterations(t, f.ctx, &stakingtypes.QueryValidatorsRequest{}, f.queryClient.Validators, 2862, false) +} + +func TestGRPCValidatorDelegations(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + validator := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + numDels := rapid.IntRange(1, 5).Draw(rt, "num-dels") + + for i := 0; i < numDels; i++ { + delegator := testdata.AddressGenerator(rt).Draw(rt, "delegator") + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegator) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := createDelegationAndDelegate(t, rt, f, delegator, validator) + assert.NilError(t, err) + } + + req := &stakingtypes.QueryValidatorDelegationsRequest{ + ValidatorAddr: validator.OperatorAddress, + Pagination: testdata.PaginationGenerator(rt, uint64(numDels)).Draw(rt, "pagination"), + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.ValidatorDelegations, 0, true) + }) + + f = initDeterministicFixture(t) // reset + + validator := getStaticValidator(t, f) + + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr1) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := fundAccountAndDelegate(t, f, delegatorAddr1, validator, f.amt1) + assert.NilError(t, err) + + acc = f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr2) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err = fundAccountAndDelegate(t, f, delegatorAddr2, validator, f.amt2) + assert.NilError(t, err) + + req := &stakingtypes.QueryValidatorDelegationsRequest{ + ValidatorAddr: validator.OperatorAddress, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.ValidatorDelegations, 14637, false) +} + +func TestGRPCValidatorUnbondingDelegations(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + validator := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + numDels := rapid.IntRange(1, 3).Draw(rt, "num-dels") + + for i := 0; i < numDels; i++ { + delegator := testdata.AddressGenerator(rt).Draw(rt, "delegator") + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegator) + f.accountKeeper.SetAccount(f.ctx, acc) + shares, err := createDelegationAndDelegate(t, rt, f, delegator, validator) + assert.NilError(t, err) + valbz, err := f.stakingKeeper.ValidatorAddressCodec().StringToBytes(validator.GetOperator()) + assert.NilError(t, err) + _, _, err = f.stakingKeeper.Undelegate(f.ctx, delegator, valbz, shares) + assert.NilError(t, err) + } + + req := &stakingtypes.QueryValidatorUnbondingDelegationsRequest{ + ValidatorAddr: validator.OperatorAddress, + Pagination: testdata.PaginationGenerator(rt, uint64(numDels)).Draw(rt, "pagination"), + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.ValidatorUnbondingDelegations, 0, true) + }) + + f = initDeterministicFixture(t) // reset + + validator := getStaticValidator(t, f) + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr1) + f.accountKeeper.SetAccount(f.ctx, acc) + shares1, err := fundAccountAndDelegate(t, f, delegatorAddr1, validator, f.amt1) + assert.NilError(t, err) + + _, _, err = f.stakingKeeper.Undelegate(f.ctx, delegatorAddr1, validatorAddr1, shares1) + assert.NilError(t, err) + + acc = f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr2) + f.accountKeeper.SetAccount(f.ctx, acc) + shares2, err := fundAccountAndDelegate(t, f, delegatorAddr2, validator, f.amt2) + assert.NilError(t, err) + + _, _, err = f.stakingKeeper.Undelegate(f.ctx, delegatorAddr2, validatorAddr1, shares2) + assert.NilError(t, err) + + req := &stakingtypes.QueryValidatorUnbondingDelegationsRequest{ + ValidatorAddr: validator.OperatorAddress, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.ValidatorUnbondingDelegations, 3719, false) +} + +func TestGRPCDelegation(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + validator := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + delegator := testdata.AddressGenerator(rt).Draw(rt, "delegator") + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegator) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := createDelegationAndDelegate(t, rt, f, delegator, validator) + assert.NilError(t, err) + + req := &stakingtypes.QueryDelegationRequest{ + ValidatorAddr: validator.OperatorAddress, + DelegatorAddr: delegator.String(), + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.Delegation, 0, true) + }) + + f = initDeterministicFixture(t) // reset + + validator := getStaticValidator(t, f) + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr1) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := fundAccountAndDelegate(t, f, delegatorAddr1, validator, f.amt1) + assert.NilError(t, err) + + req := &stakingtypes.QueryDelegationRequest{ + ValidatorAddr: validator.OperatorAddress, + DelegatorAddr: delegator1, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.Delegation, 4689, false) +} + +func TestGRPCUnbondingDelegation(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + validator := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + delegator := testdata.AddressGenerator(rt).Draw(rt, "delegator") + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegator) + f.accountKeeper.SetAccount(f.ctx, acc) + shares, err := createDelegationAndDelegate(t, rt, f, delegator, validator) + assert.NilError(t, err) + + valbz, err := f.stakingKeeper.ValidatorAddressCodec().StringToBytes(validator.GetOperator()) + assert.NilError(t, err) + _, _, err = f.stakingKeeper.Undelegate(f.ctx, delegator, valbz, shares) + assert.NilError(t, err) + + req := &stakingtypes.QueryUnbondingDelegationRequest{ + ValidatorAddr: validator.OperatorAddress, + DelegatorAddr: delegator.String(), + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.UnbondingDelegation, 0, true) + }) + + f = initDeterministicFixture(t) // reset + validator := getStaticValidator(t, f) + + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr1) + f.accountKeeper.SetAccount(f.ctx, acc) + shares1, err := fundAccountAndDelegate(t, f, delegatorAddr1, validator, f.amt1) + assert.NilError(t, err) + + _, _, err = f.stakingKeeper.Undelegate(f.ctx, delegatorAddr1, validatorAddr1, shares1) + assert.NilError(t, err) + + req := &stakingtypes.QueryUnbondingDelegationRequest{ + ValidatorAddr: validator.OperatorAddress, + DelegatorAddr: delegator1, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.UnbondingDelegation, 1621, false) +} + +func TestGRPCDelegatorDelegations(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + numVals := rapid.IntRange(1, 3).Draw(rt, "num-dels") + delegator := testdata.AddressGenerator(rt).Draw(rt, "delegator") + + for i := 0; i < numVals; i++ { + validator := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegator) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := createDelegationAndDelegate(t, rt, f, delegator, validator) + assert.NilError(t, err) + } + + req := &stakingtypes.QueryDelegatorDelegationsRequest{ + DelegatorAddr: delegator.String(), + Pagination: testdata.PaginationGenerator(rt, uint64(numVals)).Draw(rt, "pagination"), + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.DelegatorDelegations, 0, true) + }) + + f = initDeterministicFixture(t) // reset + + validator := getStaticValidator(t, f) + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr1) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := fundAccountAndDelegate(t, f, delegatorAddr1, validator, f.amt1) + assert.NilError(t, err) + + req := &stakingtypes.QueryDelegatorDelegationsRequest{ + DelegatorAddr: delegator1, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.DelegatorDelegations, 4292, false) +} + +func TestGRPCDelegatorValidator(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + validator := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + + delegator := testdata.AddressGenerator(rt).Draw(rt, "delegator") + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegator) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := createDelegationAndDelegate(t, rt, f, delegator, validator) + assert.NilError(t, err) + + req := &stakingtypes.QueryDelegatorValidatorRequest{ + DelegatorAddr: delegator.String(), + ValidatorAddr: validator.OperatorAddress, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.DelegatorValidator, 0, true) + }) + + f = initDeterministicFixture(t) // reset + + validator := getStaticValidator(t, f) + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr1) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := fundAccountAndDelegate(t, f, delegatorAddr1, validator, f.amt1) + + assert.NilError(t, err) + + req := &stakingtypes.QueryDelegatorValidatorRequest{ + DelegatorAddr: delegator1, + ValidatorAddr: validator.OperatorAddress, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.DelegatorValidator, 3563, false) +} + +func TestGRPCDelegatorUnbondingDelegations(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + numVals := rapid.IntRange(1, 5).Draw(rt, "num-vals") + delegator := testdata.AddressGenerator(rt).Draw(rt, "delegator") + + for i := 0; i < numVals; i++ { + validator := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegator) + f.accountKeeper.SetAccount(f.ctx, acc) + shares, err := createDelegationAndDelegate(t, rt, f, delegator, validator) + assert.NilError(t, err) + valbz, err := f.stakingKeeper.ValidatorAddressCodec().StringToBytes(validator.GetOperator()) + assert.NilError(t, err) + _, _, err = f.stakingKeeper.Undelegate(f.ctx, delegator, valbz, shares) + assert.NilError(t, err) + } + + req := &stakingtypes.QueryDelegatorUnbondingDelegationsRequest{ + DelegatorAddr: delegator.String(), + Pagination: testdata.PaginationGenerator(rt, uint64(numVals)).Draw(rt, "pagination"), + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.DelegatorUnbondingDelegations, 0, true) + }) + + f = initDeterministicFixture(t) // reset + + validator := getStaticValidator(t, f) + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr1) + f.accountKeeper.SetAccount(f.ctx, acc) + shares1, err := fundAccountAndDelegate(t, f, delegatorAddr1, validator, f.amt1) + assert.NilError(t, err) + + _, _, err = f.stakingKeeper.Undelegate(f.ctx, delegatorAddr1, validatorAddr1, shares1) + assert.NilError(t, err) + + req := &stakingtypes.QueryDelegatorUnbondingDelegationsRequest{ + DelegatorAddr: delegator1, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.DelegatorUnbondingDelegations, 1302, false) +} + +func TestGRPCHistoricalInfo(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + historical := stakingtypes.HistoricalRecord{} + + height := rapid.Int64Min(0).Draw(rt, "height") + + assert.NilError(t, f.stakingKeeper.HistoricalInfo.Set( + f.ctx, + uint64(height), + historical, + )) + + req := &stakingtypes.QueryHistoricalInfoRequest{ + Height: height, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.HistoricalInfo, 0, true) + }) + + f = initDeterministicFixture(t) // reset + + historicalInfo := stakingtypes.HistoricalRecord{} + + height := int64(127) + + assert.NilError(t, f.stakingKeeper.HistoricalInfo.Set( + f.ctx, + uint64(height), + historicalInfo, + )) + + req := &stakingtypes.QueryHistoricalInfoRequest{ + Height: height, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.HistoricalInfo, 1027, false) +} + +func TestGRPCDelegatorValidators(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + numVals := rapid.IntRange(1, 3).Draw(rt, "num-dels") + delegator := testdata.AddressGenerator(rt).Draw(rt, "delegator") + + for i := 0; i < numVals; i++ { + validator := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegator) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := createDelegationAndDelegate(t, rt, f, delegator, validator) + assert.NilError(t, err) + } + + req := &stakingtypes.QueryDelegatorValidatorsRequest{ + DelegatorAddr: delegator.String(), + Pagination: testdata.PaginationGenerator(rt, uint64(numVals)).Draw(rt, "pagination"), + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.DelegatorValidators, 0, true) + }) + + f = initDeterministicFixture(t) // reset + + validator := getStaticValidator(t, f) + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr1) + f.accountKeeper.SetAccount(f.ctx, acc) + _, err := fundAccountAndDelegate(t, f, delegatorAddr1, validator, f.amt1) + assert.NilError(t, err) + + req := &stakingtypes.QueryDelegatorValidatorsRequest{DelegatorAddr: delegator1} + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.DelegatorValidators, 3166, false) +} + +func TestGRPCPool(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + createAndSetValidator(t, rt, f) + + testdata.DeterministicIterations(t, f.ctx, &stakingtypes.QueryPoolRequest{}, f.queryClient.Pool, 0, true) + }) + + f = initDeterministicFixture(t) // reset + getStaticValidator(t, f) + testdata.DeterministicIterations(t, f.ctx, &stakingtypes.QueryPoolRequest{}, f.queryClient.Pool, 6296, false) +} + +func TestGRPCRedelegations(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + + rapid.Check(t, func(rt *rapid.T) { + validator := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + srcValAddr, err := sdk.ValAddressFromBech32(validator.OperatorAddress) + assert.NilError(t, err) + + validator2 := createAndSetValidatorWithStatus(t, rt, f, stakingtypes.Bonded) + dstValAddr, err := sdk.ValAddressFromBech32(validator2.OperatorAddress) + assert.NilError(t, err) + + numDels := rapid.IntRange(1, 5).Draw(rt, "num-dels") + + delegator := testdata.AddressGenerator(rt).Draw(rt, "delegator") + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegator) + f.accountKeeper.SetAccount(f.ctx, acc) + shares, err := createDelegationAndDelegate(t, rt, f, delegator, validator) + assert.NilError(t, err) + + _, err = f.stakingKeeper.BeginRedelegation(f.ctx, delegator, srcValAddr, dstValAddr, shares) + assert.NilError(t, err) + + var req *stakingtypes.QueryRedelegationsRequest + + reqType := rapid.IntRange(0, 2).Draw(rt, "req-type") + switch reqType { + case 0: // queries redelegation with delegator, source and destination validators combination. + req = &stakingtypes.QueryRedelegationsRequest{ + DelegatorAddr: delegator.String(), + SrcValidatorAddr: srcValAddr.String(), + DstValidatorAddr: dstValAddr.String(), + } + case 1: // queries redelegations of source validator. + req = &stakingtypes.QueryRedelegationsRequest{ + SrcValidatorAddr: srcValAddr.String(), + } + case 2: // queries all redelegations of a delegator. + req = &stakingtypes.QueryRedelegationsRequest{ + DelegatorAddr: delegator.String(), + } + } + + req.Pagination = testdata.PaginationGenerator(rt, uint64(numDels)).Draw(rt, "pagination") + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.Redelegations, 0, true) + }) + + f = initDeterministicFixture(t) // reset + validator := getStaticValidator(t, f) + _ = getStaticValidator2(t, f) + + acc := f.accountKeeper.NewAccountWithAddress(f.ctx, delegatorAddr1) + f.accountKeeper.SetAccount(f.ctx, acc) + shares, err := fundAccountAndDelegate(t, f, delegatorAddr1, validator, f.amt1) + assert.NilError(t, err) + + _, err = f.stakingKeeper.BeginRedelegation(f.ctx, delegatorAddr1, validatorAddr1, validatorAddr2, shares) + assert.NilError(t, err) + + req := &stakingtypes.QueryRedelegationsRequest{ + DelegatorAddr: delegator1, + SrcValidatorAddr: validator1, + DstValidatorAddr: validator2, + } + + testdata.DeterministicIterations(t, f.ctx, req, f.queryClient.Redelegations, 3920, false) +} + +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(coinDenomRegex).Draw(rt, "bond-denom") + params := stakingtypes.Params{ + BondDenom: bondDenom, + UnbondingTime: durationGenerator().Draw(rt, "duration"), + MaxValidators: rapid.Uint32Min(1).Draw(rt, "max-validators"), + MaxEntries: rapid.Uint32Min(1).Draw(rt, "max-entries"), + HistoricalEntries: rapid.Uint32Min(1).Draw(rt, "historical-entries"), + MinCommissionRate: math.LegacyNewDecWithPrec(rapid.Int64Range(0, 100).Draw(rt, "commission"), 2), + KeyRotationFee: sdk.NewInt64Coin(bondDenom, rapid.Int64Range(10000, 100000).Draw(rt, "amount")), + } + + err := f.stakingKeeper.Params.Set(f.ctx, params) + assert.NilError(t, err) + + testdata.DeterministicIterations(t, f.ctx, &stakingtypes.QueryParamsRequest{}, f.queryClient.Params, 0, true) + }) + + params := stakingtypes.Params{ + BondDenom: "denom", + UnbondingTime: time.Hour, + MaxValidators: 85, + MaxEntries: 5, + HistoricalEntries: 5, + MinCommissionRate: math.LegacyNewDecWithPrec(5, 2), + KeyRotationFee: sdk.NewInt64Coin("denom", 10000), + } + + err := f.stakingKeeper.Params.Set(f.ctx, params) + assert.NilError(t, err) + + testdata.DeterministicIterations(t, f.ctx, &stakingtypes.QueryParamsRequest{}, f.queryClient.Params, 1162, false) +} diff --git a/types/coin.go b/types/coin.go index 41a8b24f52cf..3489c96d9976 100644 --- a/types/coin.go +++ b/types/coin.go @@ -835,30 +835,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. @@ -871,9 +856,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 000000000000..ea6134a9b82e --- /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 000000000000..4a4355162b89 --- /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 f2337c1586f3..27835061bf80 100644 --- a/types/coin_test.go +++ b/types/coin_test.go @@ -112,6 +112,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 }) @@ -130,7 +132,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 42ff885d58a6..587088067e58 100644 --- a/types/dec_coin.go +++ b/types/dec_coin.go @@ -4,6 +4,7 @@ import ( "fmt" "sort" "strings" + "unicode" "github.com/pkg/errors" ) @@ -621,14 +622,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 := NewDecFromStr(amountStr) if err != nil { @@ -642,6 +652,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 6340c45c4f13..1e777887b012 100644 --- a/types/dec_coin_test.go +++ b/types/dec_coin_test.go @@ -376,6 +376,9 @@ func (s *decCoinTestSuite) TestParseDecCoins() { }{ {"", nil, false}, {"4stake", sdk.DecCoins{sdk.NewDecCoinFromDec("stake", sdk.NewDecFromInt(sdk.NewInt(4)))}, false}, + {"5.5atom", sdk.DecCoins{ + sdk.NewDecCoinFromDec("atom", math.LegacyNewDecWithPrec(5500000000000000000, math.LegacyPrecision)), + }, false}, {"5.5atom,4stake", sdk.DecCoins{ sdk.NewDecCoinFromDec("atom", sdk.NewDecWithPrec(5500000000000000000, sdk.Precision)), sdk.NewDecCoinFromDec("stake", math.LegacyNewDec(4)),