diff --git a/.github/workflows/unit-coverage.yml b/.github/workflows/unit-coverage.yml new file mode 100644 index 0000000..cb7f555 --- /dev/null +++ b/.github/workflows/unit-coverage.yml @@ -0,0 +1,22 @@ +name: Coverage +on: + push: + branches: [ main ] + pull_request: + branches: [ '**' ] +jobs: + build: + name: Unit test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + + - name: Generate unit test coverage + run: make coverage-unit + + - name: Check unit test coverage + uses: vladopajic/go-test-coverage@v2 + with: + # Configure action using config file (option 1) + config: ./.testcoverage.yml diff --git a/.gitignore b/.gitignore index f137666..65e16b7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,7 @@ release/ .exrpd/ +*.out +*.html bin/ \ No newline at end of file diff --git a/.testcoverage.yml b/.testcoverage.yml new file mode 100644 index 0000000..5434e75 --- /dev/null +++ b/.testcoverage.yml @@ -0,0 +1,55 @@ +# (mandatory) +# Path to coverage profile file (output of `go test -coverprofile` command). +# +# For cases where there are many coverage profiles, such as when running +# unit tests and integration tests separately, you can combine all those +# profiles into one. In this case, the profile should have a comma-separated list +# of profile files, e.g., 'cover_unit.out,cover_integration.out'. +profile: coverage_unit.out + +# (optional; but recommended to set) +# When specified reported file paths will not contain local prefix in the output. +local-prefix: "github.com/xrplevm/node" + +# Holds coverage thresholds percentages, values should be in range [0-100]. +threshold: + # (optional; default 0) + # Minimum overall project coverage percentage required. + total: 70 + +# Holds regexp rules which will override thresholds for matched files or packages +# using their paths. +# +# First rule from this list that matches file or package is going to apply +# new threshold to it. If project has multiple rules that match same path, +# override rules should be listed in order from specific to more general rules. +# override: + # Increase coverage threshold to 100% for `foo` package + # (default is 80, as configured above in this example). + # - path: ^pkg/lib/foo$ + # threshold: 100 + +# Holds regexp rules which will exclude matched files or packages +# from coverage statistics. +exclude: + # Exclude files or packages matching their paths + paths: + - cmd + - docs + - app + - tools + - tests + - \.pb\.go$ + - \.pb\.gw\.go$ + - module.go$ + - \/testutil\/ + - \/client\/ + +# File name of go-test-coverage breakdown file, which can be used to +# analyze coverage difference. +breakdown-file-name: '' + +diff: + # File name of go-test-coverage breakdown file which will be used to + # report coverage difference. + base-breakdown-file-name: '' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 194cddb..b583ae7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,9 @@ FROM base AS integration RUN make lint # Unit tests RUN make test-poa +# Integration tests +RUN make test-integration +RUN make test-upgrade # Simulation tests RUN make test-sim-benchmark-simulation RUN make test-sim-full-app-fast diff --git a/Makefile b/Makefile index 62da4ca..c370d0b 100644 --- a/Makefile +++ b/Makefile @@ -118,6 +118,7 @@ lint-fix: ### Testing ### ############################################################################### EXCLUDED_POA_PACKAGES=$(shell go list ./x/poa/... | grep -v /x/poa/testutil | grep -v /x/poa/client | grep -v /x/poa/simulation | grep -v /x/poa/types) +EXCLUDED_UNIT_PACKAGES=$(shell go list ./... | grep -v tests | grep -v testutil | grep -v tools | grep -v app | grep -v docs | grep -v cmd | grep -v /x/poa/testutil | grep -v /x/poa/client | grep -v /x/poa/simulation | grep -v /x/poa/types) mocks: @echo "--> Installing mockgen" @@ -125,7 +126,15 @@ mocks: @echo "--> Generating mocks" @./scripts/mockgen.sh -test: test-poa test-sim-benchmark-simulation test-sim-full-app-fast +test: test-poa test-integration test-upgrade test-sim-benchmark-simulation test-sim-full-app-fast + +test-upgrade: + @echo "--> Running upgrade testsuite" + @go test -mod=readonly -v ./tests/upgrade + +test-integration: + @echo "--> Running integration testsuite" + @go test -mod=readonly -v ./tests/integration test-poa: @echo "--> Running POA tests" @@ -142,6 +151,25 @@ test-sim-full-app-fast: @cd ${CURDIR}/app && go test -mod=readonly -run TestFullAppSimulation \ -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Period=5 -Params=${CURDIR}/tests/sim/params.json -v -timeout 24h +############################################################################### +### Coverage ### +############################################################################### + +coverage-unit: + @echo "--> Running unit coverage" + @go test $(EXCLUDED_UNIT_PACKAGES) -coverprofile=coverage_unit.out > /dev/null + @go tool cover -func=coverage_unit.out + +coverage-poa: + @echo "--> Running POA coverage" + @go test $(EXCLUDED_POA_PACKAGES) -coverprofile=coverage_poa.out > /dev/null + @go tool cover -func=coverage_poa.out + +coverage-integration: + @echo "--> Running integration coverage" + @go test ./tests/integration -mod=readonly -coverprofile=coverage_integration.out > /dev/null + @go tool cover -func=coverage_integration.out + ############################################################################### ### Protobuf ### ############################################################################### diff --git a/tests/integration/network.go b/tests/integration/network.go new file mode 100644 index 0000000..c71a799 --- /dev/null +++ b/tests/integration/network.go @@ -0,0 +1,79 @@ +package integration + +import ( + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + erc20types "github.com/evmos/evmos/v20/x/erc20/types" + evmtypes "github.com/evmos/evmos/v20/x/evm/types" + feemarkettypes "github.com/evmos/evmos/v20/x/feemarket/types" + commonnetwork "github.com/xrplevm/node/v6/testutil/integration/common/network" + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" + exrpintegration "github.com/xrplevm/node/v6/testutil/integration/exrp/integration" + poatypes "github.com/xrplevm/node/v6/x/poa/types" +) + +var _ commonnetwork.Network = (*Network)(nil) + +type Network struct { + exrpintegration.IntegrationNetwork +} + +func NewIntegrationNetwork(opts ...exrpcommon.ConfigOption) *Network { + network := exrpintegration.New(opts...) + return &Network{ + IntegrationNetwork: *network, + } +} + +func (n *Network) SetupSdkConfig() { + exrpcommon.SetupSdkConfig() +} + +func (n *Network) GetERC20Client() erc20types.QueryClient { + return exrpcommon.GetERC20Client(n) +} + +func (n *Network) GetEvmClient() evmtypes.QueryClient { + return exrpcommon.GetEvmClient(n) +} + +func (n *Network) GetGovClient() govtypes.QueryClient { + return exrpcommon.GetGovClient(n) +} + +func (n *Network) GetBankClient() banktypes.QueryClient { + return exrpcommon.GetBankClient(n) +} + +func (n *Network) GetFeeMarketClient() feemarkettypes.QueryClient { + return exrpcommon.GetFeeMarketClient(n) +} + +func (n *Network) GetAuthClient() authtypes.QueryClient { + return exrpcommon.GetAuthClient(n) +} + +func (n *Network) GetAuthzClient() authz.QueryClient { + return exrpcommon.GetAuthzClient(n) +} + +func (n *Network) GetStakingClient() stakingtypes.QueryClient { + return exrpcommon.GetStakingClient(n) +} + +func (n *Network) GetSlashingClient() slashingtypes.QueryClient { + return exrpcommon.GetSlashingClient(n) +} + +func (n *Network) GetDistrClient() distrtypes.QueryClient { + return exrpcommon.GetDistrClient(n) +} + +func (n *Network) GetPoaClient() poatypes.QueryClient { + return exrpcommon.GetPoaClient(n) +} diff --git a/tests/integration/poa_test.go b/tests/integration/poa_test.go new file mode 100644 index 0000000..0c8f8c2 --- /dev/null +++ b/tests/integration/poa_test.go @@ -0,0 +1,1037 @@ +package integration + +import ( + "fmt" + "math/rand" + "time" + + abcitypes "github.com/cometbft/cometbft/abci/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + "github.com/xrplevm/node/v6/testutil/integration/exrp/utils" + poatypes "github.com/xrplevm/node/v6/x/poa/types" +) + +// AddValidator tests + +func (s *TestSuite) TestAddValidator_UnexistingValidator() { + // Generate a random account + randomAccs := simtypes.RandomAccounts(rand.New(rand.NewSource(time.Now().UnixNano())), 1) //nolint:gosec + randomAcc := randomAccs[0] + randomValAddr := sdktypes.ValAddress(randomAcc.Address.Bytes()) + + tt := []struct { + name string + valAddress string + valPubKey cryptotypes.PubKey + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "add unexisting validator - random address", + valAddress: randomAcc.Address.String(), + valPubKey: randomAcc.ConsKey.PubKey(), + afterRun: func() { + require.NoError(s.T(), s.Network().NextBlock()) + + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: randomValAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is bonded + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Bonded) + + // Check if the validator has the default amount of tokens + require.Equal(s.T(), sdktypes.DefaultPowerReduction, resVal.Validator.Tokens) + + // Check if the validator has the default delegator shares + require.Equal(s.T(), sdktypes.DefaultPowerReduction.ToLegacyDec(), resVal.Validator.DelegatorShares) + }, + }, + } + + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg, err := poatypes.NewMsgAddValidator( + authority.String(), + tc.valAddress, + tc.valPubKey, + stakingtypes.Description{ + Moniker: "test", + }, + ) + require.NoError(s.T(), err) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_PASSED, proposal.Status) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} + +func (s *TestSuite) TestAddValidator_InvalidMsgAddValidator() { + // Generate a random account + randomAccs := simtypes.RandomAccounts(rand.New(rand.NewSource(time.Now().UnixNano())), 1) //nolint:gosec + randomAcc := randomAccs[0] + + validator := s.Network().GetValidators()[0] + valAddr, err := sdktypes.ValAddressFromBech32(validator.OperatorAddress) + require.NoError(s.T(), err) + valAccAddr := sdktypes.AccAddress(valAddr) + + tt := []struct { + name string + valAddress string + valPubKey cryptotypes.PubKey + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "add validator - already used pubkey", + valAddress: randomAcc.Address.String(), + valPubKey: validator.ConsensusPubkey.GetCachedValue().(cryptotypes.PubKey), + expectedError: stakingtypes.ErrValidatorPubKeyExists, + }, + { + name: "add validator - already used validator address", + valAddress: valAccAddr.String(), + valPubKey: randomAcc.ConsKey.PubKey(), + expectedError: poatypes.ErrAddressHasBondedTokens, + beforeRun: func() { + // Check if the validator exists + _, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + }, + }, + } + + //nolint:dupl + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg, err := poatypes.NewMsgAddValidator( + authority.String(), + tc.valAddress, + tc.valPubKey, + stakingtypes.Description{ + Moniker: "test", + }, + ) + require.NoError(s.T(), err) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_FAILED, proposal.Status) + require.Contains(s.T(), proposal.FailedReason, tc.expectedError.Error()) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} + +func (s *TestSuite) TestAddValidator_ExistingValidator_StatusBonded() { + validator := s.Network().GetValidators()[0] + valAddr, err := sdktypes.ValAddressFromBech32(validator.OperatorAddress) + require.NoError(s.T(), err) + valAccAddr := sdktypes.AccAddress(valAddr) + + tt := []struct { + name string + valAddress string + valPubKey cryptotypes.PubKey + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "add existing validator - status bonded", + valAddress: valAccAddr.String(), + valPubKey: validator.ConsensusPubkey.GetCachedValue().(cryptotypes.PubKey), + expectedError: poatypes.ErrAddressHasBondedTokens, + beforeRun: func() { + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is bonded + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Bonded) + }, + afterRun: func() { + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is still bonded + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Bonded) + }, + }, + } + + //nolint:dupl + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg, err := poatypes.NewMsgAddValidator( + authority.String(), + tc.valAddress, + tc.valPubKey, + stakingtypes.Description{ + Moniker: "test", + }, + ) + require.NoError(s.T(), err) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_FAILED, proposal.Status) + require.Contains(s.T(), proposal.FailedReason, tc.expectedError.Error()) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} + +func (s *TestSuite) TestAddValidator_ExistingValidator_Jailed() { + valIndex := 1 + validator := s.Network().GetValidators()[valIndex] + valAddr, err := sdktypes.ValAddressFromBech32(validator.OperatorAddress) + require.NoError(s.T(), err) + valAccAddr := sdktypes.AccAddress(valAddr) + + tt := []struct { + name string + valAddress string + valPubKey cryptotypes.PubKey + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "add existing validator - status jailed", + valAddress: valAccAddr.String(), + valPubKey: validator.ConsensusPubkey.GetCachedValue().(cryptotypes.PubKey), + expectedError: poatypes.ErrAddressHasBondedTokens, + beforeRun: func() { + // Force jail validator + valSet := s.Network().GetValidatorSet() + + require.NoError( + s.T(), + s.Network().NextNBlocksWithValidatorFlags( + slashingtypes.DefaultSignedBlocksWindow, + utils.NewValidatorFlags( + len(valSet.Validators), + utils.NewValidatorFlagOverride(valIndex, cmtproto.BlockIDFlagAbsent), + ), + ), + ) + + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is jailed + require.Equal(s.T(), resVal.Validator.Jailed, true) + + // Check if the validator is unbonding + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Unbonding) + }, + afterRun: func() { + // Check if the validator is still jailed + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is jailed + require.Equal(s.T(), resVal.Validator.Jailed, true) + + // Check if the validator is unbonding + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Unbonding) + }, + }, + } + + //nolint:dupl + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg, err := poatypes.NewMsgAddValidator( + authority.String(), + tc.valAddress, + tc.valPubKey, + stakingtypes.Description{ + Moniker: "test", + }, + ) + require.NoError(s.T(), err) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_FAILED, proposal.Status) + require.Contains(s.T(), proposal.FailedReason, tc.expectedError.Error()) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} + +func (s *TestSuite) TestAddValidator_ExistingValidator_Tombstoned() { + valIndex := 1 + + // CometBFT validators + valSet := s.Network().GetValidatorSet() + cmtValAddr := sdktypes.AccAddress(valSet.Validators[valIndex].Address.Bytes()) + cmtValConsAddr := sdktypes.ConsAddress(valSet.Validators[valIndex].Address.Bytes()) + + // Cosmos validators + validators := s.Network().GetValidators() + require.NotZero(s.T(), len(validators)) + + validator := validators[valIndex] + valAddr, err := sdktypes.ValAddressFromBech32(validator.OperatorAddress) + require.NoError(s.T(), err) + valAccAddr := sdktypes.AccAddress(valAddr) + + tt := []struct { + name string + valAddress string + valPubKey cryptotypes.PubKey + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "add existing validator - status tombstoned", + valAddress: valAccAddr.String(), + valPubKey: validator.ConsensusPubkey.GetCachedValue().(cryptotypes.PubKey), + expectedError: poatypes.ErrAddressHasBondedTokens, + beforeRun: func() { + // Force validator to be tombstoned + require.NoError(s.T(), s.Network().NextBlockWithMisBehaviors( + []abcitypes.Misbehavior{ + { + Type: abcitypes.MisbehaviorType_DUPLICATE_VOTE, + Validator: abcitypes.Validator{ + Address: cmtValAddr.Bytes(), + }, + Height: s.Network().GetContext().BlockHeight(), + TotalVotingPower: s.Network().GetValidatorSet().TotalVotingPower(), + }, + }, + )) + + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is jailed + require.Equal(s.T(), resVal.Validator.Jailed, true) + + // Check if the validator is unbonding + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Unbonding) + + info, err := s.Network().GetSlashingClient().SigningInfo( + s.Network().GetContext(), + &slashingtypes.QuerySigningInfoRequest{ + ConsAddress: cmtValConsAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is tombstoned + require.Equal(s.T(), info.ValSigningInfo.Tombstoned, true) + }, + afterRun: func() { + // Check if the validator is still jailed + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is jailed + require.Equal(s.T(), resVal.Validator.Jailed, true) + + // Check if the validator is unbonding + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Unbonding) + }, + }, + } + + //nolint:dupl + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg, err := poatypes.NewMsgAddValidator( + authority.String(), + tc.valAddress, + tc.valPubKey, + stakingtypes.Description{ + Moniker: "test", + }, + ) + require.NoError(s.T(), err) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_FAILED, proposal.Status) + require.Contains(s.T(), proposal.FailedReason, tc.expectedError.Error()) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} + +func (s *TestSuite) TestAddValidator_MaximumValidators() { + // Generate a random account + randomAccs := simtypes.RandomAccounts(rand.New(rand.NewSource(time.Now().UnixNano())), 1) //nolint:gosec + randomAcc := randomAccs[0] + randomValAddr := sdktypes.ValAddress(randomAcc.Address.Bytes()) + + tt := []struct { + name string + valAddress string + valPubKey cryptotypes.PubKey + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "add validator - maximum validators reached", + valAddress: randomAcc.Address.String(), + valPubKey: randomAcc.PubKey, + expectedError: poatypes.ErrMaxValidatorsReached, + beforeRun: func() { + resVal, err := s.Network().GetStakingClient().Params( + s.Network().GetContext(), + &stakingtypes.QueryParamsRequest{}, + ) + s.Require().NoError(err) + amountOfValidators := uint32(5) + maxValidators := resVal.Params.MaxValidators + authority := sdktypes.AccAddress(address.Module("gov")).String() + + for i := uint32(0); i < maxValidators-amountOfValidators; i++ { + randomValidator := simtypes.RandomAccounts(rand.New(rand.NewSource(time.Now().UnixNano())), 1) //nolint:gosec + randomValidatorAcc := randomValidator[0] + msg, err := poatypes.NewMsgAddValidator( + authority, + randomValidatorAcc.Address.String(), + randomValidatorAcc.ConsKey.PubKey(), + stakingtypes.Description{ + Moniker: "test", + }, + ) + require.NoError(s.T(), err) + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_PASSED, proposal.Status) + } + }, + afterRun: func() { + // Check validator not added + _, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: randomValAddr.String(), + }, + ) + require.Error(s.T(), err) + }, + }, + } + + //nolint:dupl + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg, err := poatypes.NewMsgAddValidator( + authority.String(), + tc.valAddress, + tc.valPubKey, + stakingtypes.Description{ + Moniker: "test", + }, + ) + require.NoError(s.T(), err) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_FAILED, proposal.Status) + require.Contains(s.T(), proposal.FailedReason, tc.expectedError.Error()) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} + +// RemoveValidator tests + +func (s *TestSuite) TestRemoveValidator_UnexistingValidator() { + // Generate a random account + randomAccs := simtypes.RandomAccounts(rand.New(rand.NewSource(time.Now().UnixNano())), 1) //nolint:gosec + + randomValAddr := sdktypes.ValAddress(randomAccs[0].Address.Bytes()) + + tt := []struct { + name string + valAddress string + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "remove unexisting validator - random address - with balance", + valAddress: randomValAddr.String(), + expectedError: poatypes.ErrAddressIsNotAValidator, + beforeRun: func() { + _, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: randomValAddr.String(), + }, + ) + + // Check if the validator does not exist + require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", randomValAddr.String())) + }, + afterRun: func() { + _, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: randomValAddr.String(), + }, + ) + + // Check if the validator is not found + require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", randomValAddr.String())) + }, + }, + } + + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg := poatypes.NewMsgRemoveValidator( + authority.String(), + tc.valAddress, + ) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_FAILED, proposal.Status) + require.Equal(s.T(), tc.expectedError.Error(), proposal.FailedReason) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} + +func (s *TestSuite) TestRemoveValidator_ExistingValidator_StatusBonded() { + // Validators + validators := s.Network().GetValidators() + require.NotZero(s.T(), len(validators)) + + validator := validators[0] + valAddr, err := sdktypes.ValAddressFromBech32(validator.OperatorAddress) + require.NoError(s.T(), err) + + tt := []struct { + name string + valAddress string + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "remove existing validator - status bonded", + valAddress: valAddr.String(), + beforeRun: func() { + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is bonded + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Bonded) + + // Check if the validator has delegator shares + require.Equal(s.T(), sdktypes.DefaultPowerReduction.ToLegacyDec(), resVal.Validator.DelegatorShares) + + // Check if the validator has tokens + require.NotZero(s.T(), resVal.Validator.Tokens) + }, + afterRun: func() { + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator has delegator shares + require.True(s.T(), + resVal.Validator.DelegatorShares.IsZero(), + "delegator shares should be zero, got %s", + resVal.Validator.DelegatorShares, + ) + + // Check if the validator has no tokens + require.True(s.T(), resVal.Validator.Tokens.IsZero()) + + resVal, err = s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is unbonded + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Unbonding) + + require.NoError(s.T(), s.Network().NextBlockAfter(stakingtypes.DefaultUnbondingTime)) + + _, err = s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", valAddr.String())) + }, + }, + } + + //nolint:dupl + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg := poatypes.NewMsgRemoveValidator( + authority.String(), + valAddr.String(), + ) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_PASSED, proposal.Status) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} + +func (s *TestSuite) TestRemoveValidator_ExistingValidator_Jailed() { + // Validators + validators := s.Network().GetValidators() + require.NotZero(s.T(), len(validators)) + + valIndex := 1 + validator := validators[valIndex] + valAddr, err := sdktypes.ValAddressFromBech32(validator.OperatorAddress) + require.NoError(s.T(), err) + + tt := []struct { + name string + valAddress string + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "remove existing validator - jailed", + valAddress: valAddr.String(), + beforeRun: func() { + // Force jail validator + valSet := s.Network().GetValidatorSet() + + require.NoError( + s.T(), + s.Network().NextNBlocksWithValidatorFlags( + slashingtypes.DefaultSignedBlocksWindow, + utils.NewValidatorFlags( + len(valSet.Validators), + utils.NewValidatorFlagOverride(valIndex, cmtproto.BlockIDFlagAbsent), + ), + ), + ) + + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is jailed + require.Equal(s.T(), resVal.Validator.Jailed, true) + + // Check if the validator is unbonding + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Unbonding) + }, + afterRun: func() { + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator has delegator shares + require.True(s.T(), resVal.Validator.DelegatorShares.IsZero()) + + // Check if the validator has no tokens + require.True(s.T(), resVal.Validator.Tokens.IsZero()) + + // Check if the validator is unbonding + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Unbonding) + + require.NoError(s.T(), s.Network().NextBlockAfter(stakingtypes.DefaultUnbondingTime)) + + _, err = s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + + require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", valAddr.String())) + }, + }, + } + + //nolint:dupl + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg := poatypes.NewMsgRemoveValidator( + authority.String(), + valAddr.String(), + ) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_PASSED, proposal.Status) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} + +func (s *TestSuite) TestRemoveValidator_ExistingValidator_Tombstoned() { + valIndex := 1 + + // CometBFT validators + valSet := s.Network().GetValidatorSet() + cmtValAddr := sdktypes.AccAddress(valSet.Validators[valIndex].Address.Bytes()) + cmtValConsAddr := sdktypes.ConsAddress(valSet.Validators[valIndex].Address.Bytes()) + + // Cosmos validators + validators := s.Network().GetValidators() + require.NotZero(s.T(), len(validators)) + + validator := validators[valIndex] + valAddr, err := sdktypes.ValAddressFromBech32(validator.OperatorAddress) + require.NoError(s.T(), err) + + tt := []struct { + name string + valAddress string + expectedError error + beforeRun func() + afterRun func() + }{ + { + name: "remove existing validator - tombstoned", + valAddress: valAddr.String(), + beforeRun: func() { + // Force validator to be tombstoned + require.NoError(s.T(), s.Network().NextBlockWithMisBehaviors( + []abcitypes.Misbehavior{ + { + Type: abcitypes.MisbehaviorType_DUPLICATE_VOTE, + Validator: abcitypes.Validator{ + Address: cmtValAddr, + }, + Height: s.Network().GetContext().BlockHeight(), + TotalVotingPower: s.Network().GetValidatorSet().TotalVotingPower(), + }, + }, + )) + + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is jailed + require.Equal(s.T(), resVal.Validator.Jailed, true) + + // Check if the validator is unbonding + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Unbonding) + + info, err := s.Network().GetSlashingClient().SigningInfo( + s.Network().GetContext(), + &slashingtypes.QuerySigningInfoRequest{ + ConsAddress: cmtValConsAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is tombstoned + require.Equal(s.T(), info.ValSigningInfo.Tombstoned, true) + }, + afterRun: func() { + resVal, err := s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator has delegator shares + require.True(s.T(), resVal.Validator.DelegatorShares.IsZero()) + + // Check if the validator has no tokens + require.True(s.T(), resVal.Validator.Tokens.IsZero()) + + // Check if the validator is unbonding + require.Equal(s.T(), resVal.Validator.Status, stakingtypes.Unbonding) + + // Await unbonding time to pass + require.NoError(s.T(), s.Network().NextBlockAfter(stakingtypes.DefaultUnbondingTime)) + + _, err = s.Network().GetStakingClient().Validator( + s.Network().GetContext(), + &stakingtypes.QueryValidatorRequest{ + ValidatorAddr: valAddr.String(), + }, + ) + + // Check if the validator is not found + require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", valAddr.String())) + + info, err := s.Network().GetSlashingClient().SigningInfo( + s.Network().GetContext(), + &slashingtypes.QuerySigningInfoRequest{ + ConsAddress: cmtValConsAddr.String(), + }, + ) + require.NoError(s.T(), err) + + // Check if the validator is tombstoned + require.True(s.T(), info.ValSigningInfo.Tombstoned) + }, + }, + } + + //nolint:dupl + for _, tc := range tt { + s.Run(tc.name, func() { + if tc.beforeRun != nil { + tc.beforeRun() + } + + authority := sdktypes.AccAddress(address.Module("gov")) + msg := poatypes.NewMsgRemoveValidator( + authority.String(), + valAddr.String(), + ) + + proposal, err := utils.SubmitAndAwaitProposalResolution(s.factory, s.Network(), s.keyring.GetKeys(), "test", msg) + require.NoError(s.T(), err) + + require.Equal(s.T(), govv1.ProposalStatus_PROPOSAL_STATUS_PASSED, proposal.Status) + + if tc.expectedError != nil && err != nil { + require.Error(s.T(), err) + require.ErrorIs(s.T(), err, tc.expectedError) + } else { + require.NoError(s.T(), err) + } + + if tc.afterRun != nil { + tc.afterRun() + } + }) + } +} diff --git a/tests/integration/suite.go b/tests/integration/suite.go new file mode 100644 index 0000000..662727d --- /dev/null +++ b/tests/integration/suite.go @@ -0,0 +1,61 @@ +package integration + +import ( + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + evmtypes "github.com/evmos/evmos/v20/x/evm/types" + "github.com/stretchr/testify/suite" + "github.com/xrplevm/node/v6/app" + factory "github.com/xrplevm/node/v6/testutil/integration/common/factory" + "github.com/xrplevm/node/v6/testutil/integration/common/grpc" + "github.com/xrplevm/node/v6/testutil/integration/common/keyring" + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" +) + +type TestSuite struct { + suite.Suite + + network *Network + keyring keyring.Keyring + factory factory.CoreTxFactory + grpcHandler grpc.Handler +} + +func (s *TestSuite) Network() *Network { + return s.network +} + +func (s *TestSuite) SetupSuite() { + s.network.SetupSdkConfig() + s.Require().Equal(sdk.GetConfig().GetBech32AccountAddrPrefix(), "ethm") +} + +func (s *TestSuite) SetupTest() { + // Check that the network was created successfully + kr := keyring.New(5) + + customGenesis := exrpcommon.CustomGenesisState{} + + evmGen := evmtypes.DefaultGenesisState() + + evmGen.Params.EvmDenom = app.BaseDenom + + customGenesis[evmtypes.ModuleName] = evmGen + + s.network = NewIntegrationNetwork( + exrpcommon.WithPreFundedAccounts(kr.GetAllAccAddrs()...), + exrpcommon.WithAmountOfValidators(5), + exrpcommon.WithCustomGenesis(customGenesis), + exrpcommon.WithBondDenom("apoa"), + exrpcommon.WithMaxValidators(7), + exrpcommon.WithMinDepositAmt(sdkmath.NewInt(1)), + exrpcommon.WithValidatorOperators(kr.GetAllAccAddrs()), + ) + s.Require().NotNil(s.network) + + grpcHandler := grpc.NewIntegrationHandler(s.network) + + s.factory = factory.New(s.network, grpcHandler) + s.keyring = kr + s.grpcHandler = grpcHandler +} diff --git a/tests/integration/suite_test.go b/tests/integration/suite_test.go new file mode 100644 index 0000000..fc13aa6 --- /dev/null +++ b/tests/integration/suite_test.go @@ -0,0 +1,11 @@ +package integration + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(TestSuite)) +} diff --git a/tests/upgrade/README.md b/tests/upgrade/README.md new file mode 100644 index 0000000..0a05141 --- /dev/null +++ b/tests/upgrade/README.md @@ -0,0 +1,14 @@ +# Upgrade testsuite + +## Download exported state + +```bash + +``` + +## Setup + +Set the `UPGRADE_STATE_FILE` environment variable to the path to the exported state file. +```bash +UPGRADE_STATE_FILE="path/to/exported-state.json" +``` \ No newline at end of file diff --git a/tests/upgrade/network.go b/tests/upgrade/network.go new file mode 100644 index 0000000..960cbab --- /dev/null +++ b/tests/upgrade/network.go @@ -0,0 +1,74 @@ +package testupgrade + +import ( + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + erc20types "github.com/evmos/evmos/v20/x/erc20/types" + evmtypes "github.com/evmos/evmos/v20/x/evm/types" + feemarkettypes "github.com/evmos/evmos/v20/x/feemarket/types" + commonnetwork "github.com/xrplevm/node/v6/testutil/integration/common/network" + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" + upgradenetwork "github.com/xrplevm/node/v6/testutil/integration/exrp/upgrade" + poatypes "github.com/xrplevm/node/v6/x/poa/types" +) + +var _ commonnetwork.Network = (*UpgradeTestNetwork)(nil) + +type UpgradeTestNetwork struct { + upgradenetwork.UpgradeIntegrationNetwork +} + +func NewUpgradeTestNetwork(opts ...exrpcommon.ConfigOption) *UpgradeTestNetwork { + network := upgradenetwork.New(opts...) + return &UpgradeTestNetwork{ + UpgradeIntegrationNetwork: *network, + } +} + +func (n *UpgradeTestNetwork) SetupSdkConfig() { + exrpcommon.SetupSdkConfig() +} + +func (n *UpgradeTestNetwork) GetERC20Client() erc20types.QueryClient { + return exrpcommon.GetERC20Client(n) +} + +func (n *UpgradeTestNetwork) GetEvmClient() evmtypes.QueryClient { + return exrpcommon.GetEvmClient(n) +} + +func (n *UpgradeTestNetwork) GetGovClient() govtypes.QueryClient { + return exrpcommon.GetGovClient(n) +} + +func (n *UpgradeTestNetwork) GetBankClient() banktypes.QueryClient { + return exrpcommon.GetBankClient(n) +} + +func (n *UpgradeTestNetwork) GetFeeMarketClient() feemarkettypes.QueryClient { + return exrpcommon.GetFeeMarketClient(n) +} + +func (n *UpgradeTestNetwork) GetAuthClient() authtypes.QueryClient { + return exrpcommon.GetAuthClient(n) +} + +func (n *UpgradeTestNetwork) GetAuthzClient() authz.QueryClient { + return exrpcommon.GetAuthzClient(n) +} + +func (n *UpgradeTestNetwork) GetStakingClient() stakingtypes.QueryClient { + return exrpcommon.GetStakingClient(n) +} + +func (n *UpgradeTestNetwork) GetDistrClient() distrtypes.QueryClient { + return exrpcommon.GetDistrClient(n) +} + +func (n *UpgradeTestNetwork) GetPoaClient() poatypes.QueryClient { + return exrpcommon.GetPoaClient(n) +} diff --git a/tests/upgrade/suite.go b/tests/upgrade/suite.go new file mode 100644 index 0000000..ec76fc4 --- /dev/null +++ b/tests/upgrade/suite.go @@ -0,0 +1,43 @@ +package testupgrade + +import ( + "os" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + exrpupgrade "github.com/xrplevm/node/v6/testutil/integration/exrp/upgrade" +) + +const defaultStateFile = "upgrade-state.json" + +type UpgradeTestSuite struct { + suite.Suite + + network *UpgradeTestNetwork +} + +func (s *UpgradeTestSuite) Network() *UpgradeTestNetwork { + return s.network +} + +func (s *UpgradeTestSuite) SetupTest() { + // Get the state file from the environment variable, or use the default one + stateFile := os.Getenv("UPGRADE_STATE_FILE") + if stateFile == "" { + stateFile = defaultStateFile + } + s.Require().NotEmpty(stateFile) + + // Setup the SDK config + s.network.SetupSdkConfig() + + s.Require().Equal(sdk.GetConfig().GetBech32AccountAddrPrefix(), "ethm") + + // Create the network + s.network = NewUpgradeTestNetwork( + exrpupgrade.WithGenesisFile(stateFile), + ) + + // Check that the network was created successfully + s.Require().NotNil(s.network) +} diff --git a/tests/upgrade/suite_test.go b/tests/upgrade/suite_test.go new file mode 100644 index 0000000..a5513a2 --- /dev/null +++ b/tests/upgrade/suite_test.go @@ -0,0 +1,36 @@ +package testupgrade + +import ( + "testing" + + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/suite" + "github.com/xrplevm/node/v6/app" +) + +func TestUpgradeTestSuite(t *testing.T) { + suite.Run(t, new(UpgradeTestSuite)) +} + +func (s *UpgradeTestSuite) TestUpgrade() { + denom := s.network.GetDenom() + s.Require().NotEmpty(denom) + s.Require().Equal(denom, app.BaseDenom) + + balances, err := s.Network().GetBankClient().AllBalances(s.network.GetContext(), &banktypes.QueryAllBalancesRequest{ + Address: "ethm1fl48vsnmsdzcv85q5d2q4z5ajdha8yu3w48d64", + }) + + s.T().Log("balances", balances) + s.Require().NoError(err) + + err = s.network.NextBlock() + s.Require().NoError(err) + + res, err := s.Network().GetStakingClient().Validators(s.network.GetContext(), &stakingtypes.QueryValidatorsRequest{}) + s.Require().NoError(err) + + s.T().Log("validators", len(res.Validators)) + s.Require().Equal(len(res.Validators), 1) +} diff --git a/tests/upgrade/upgrade-state.json b/tests/upgrade/upgrade-state.json new file mode 100644 index 0000000..af21608 --- /dev/null +++ b/tests/upgrade/upgrade-state.json @@ -0,0 +1,656 @@ +{ + "app_name": "exrpd", + "app_version": "v4.0.0-7-g40ab899", + "genesis_time": "2024-11-27T17:14:04.871328Z", + "chain_id": "exrp_1440002-1", + "initial_height": 8, + "app_hash": null, + "app_state": { + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.ModuleAccount", + "base_account": { + "address": "ethm1glht96kr2rseywuvhhay894qw7ekuc4q32ac2y", + "pub_key": null, + "account_number": "7", + "sequence": "0" + }, + "name": "erc20", + "permissions": [ + "minter", + "burner" + ] + }, + { + "@type": "/cosmos.auth.v1beta1.ModuleAccount", + "base_account": { + "address": "ethm1fl48vsnmsdzcv85q5d2q4z5ajdha8yu3w48d64", + "pub_key": null, + "account_number": "3", + "sequence": "0" + }, + "name": "bonded_tokens_pool", + "permissions": [ + "burner", + "staking" + ] + }, + { + "@type": "/cosmos.auth.v1beta1.ModuleAccount", + "base_account": { + "address": "ethm1tygms3xhhs3yv487phx3dw4a95jn7t7l64muvp", + "pub_key": null, + "account_number": "4", + "sequence": "0" + }, + "name": "not_bonded_tokens_pool", + "permissions": [ + "burner", + "staking" + ] + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "ethm1dakgyqjulg29m5fmv992g2y66m9g2mjn6hahwg", + "pub_key": { + "@type": "/ethermint.crypto.v1.ethsecp256k1.PubKey", + "key": "AyzS/NPje8oX+4+4uWP9f3060duAFXh3MpNWELtTxPz1" + }, + "account_number": "0", + "sequence": "1" + }, + { + "@type": "/cosmos.auth.v1beta1.ModuleAccount", + "base_account": { + "address": "ethm1wztruvatpslu6ngetc65272cshcnzsxgphqyau", + "pub_key": null, + "account_number": "8", + "sequence": "0" + }, + "name": "poa", + "permissions": [ + "minter", + "burner" + ] + }, + { + "@type": "/cosmos.auth.v1beta1.ModuleAccount", + "base_account": { + "address": "ethm10d07y265gmmuvt4z0w9aw880jnsr700jpva843", + "pub_key": null, + "account_number": "5", + "sequence": "0" + }, + "name": "gov", + "permissions": [ + "burner" + ] + }, + { + "@type": "/cosmos.auth.v1beta1.ModuleAccount", + "base_account": { + "address": "ethm1jv65s3grqf6v6jl3dp4t6c9t9rk99cd8u3272a", + "pub_key": null, + "account_number": "2", + "sequence": "0" + }, + "name": "distribution", + "permissions": [] + }, + { + "@type": "/ethermint.types.v1.EthAccount", + "base_account": { + "address": "ethm16j2fvexdsfnq4t5ehmwqxjsda29qh4gh75fmg2", + "pub_key": null, + "account_number": "6", + "sequence": "0" + }, + "code_hash": "0x7b477c761b4d0469f03f27ba58d0a7eacbfdd62b69b82c6c683ae5f81c67fe80" + }, + { + "@type": "/cosmos.auth.v1beta1.ModuleAccount", + "base_account": { + "address": "ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl", + "pub_key": null, + "account_number": "1", + "sequence": "0" + }, + "name": "fee_collector", + "permissions": [] + } + ] + }, + "authz": { + "authorization": [] + }, + "bank": { + "params": { + "send_enabled": [], + "default_send_enabled": true + }, + "balances": [ + { + "address": "ethm1fl48vsnmsdzcv85q5d2q4z5ajdha8yu3w48d64", + "coins": [ + { + "denom": "apoa", + "amount": "1000000" + } + ] + }, + { + "address": "ethm1dakgyqjulg29m5fmv992g2y66m9g2mjn6hahwg", + "coins": [ + { + "denom": "token", + "amount": "1000000000000000000000000000" + } + ] + } + ], + "supply": [ + { + "denom": "apoa", + "amount": "1000000" + }, + { + "denom": "token", + "amount": "1000000000000000000000000000" + } + ], + "denom_metadata": [], + "send_enabled": [] + }, + "capability": { + "index": "3", + "owners": [ + { + "index": "1", + "index_owners": { + "owners": [ + { + "module": "ibc", + "name": "ports/transfer" + }, + { + "module": "transfer", + "name": "ports/transfer" + } + ] + } + }, + { + "index": "2", + "index_owners": { + "owners": [ + { + "module": "ibc", + "name": "ports/icahost" + }, + { + "module": "icahost", + "name": "ports/icahost" + } + ] + } + } + ] + }, + "crisis": { + "constant_fee": { + "denom": "token", + "amount": "1000" + } + }, + "distribution": { + "params": { + "community_tax": "0.020000000000000000", + "base_proposer_reward": "0.000000000000000000", + "bonus_proposer_reward": "0.000000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "ethmvalcons123s7tvluhrla7tpj5mwysxqlcgwwzwavxenru7", + "outstanding_rewards": [ + { + "validator_address": "ethmvaloper1dakgyqjulg29m5fmv992g2y66m9g2mjn48hmk4", + "outstanding_rewards": [] + } + ], + "validator_accumulated_commissions": [ + { + "validator_address": "ethmvaloper1dakgyqjulg29m5fmv992g2y66m9g2mjn48hmk4", + "accumulated": { + "commission": [] + } + } + ], + "validator_historical_rewards": [ + { + "validator_address": "ethmvaloper1dakgyqjulg29m5fmv992g2y66m9g2mjn48hmk4", + "period": "1", + "rewards": { + "cumulative_reward_ratio": [], + "reference_count": 2 + } + } + ], + "validator_current_rewards": [ + { + "validator_address": "ethmvaloper1dakgyqjulg29m5fmv992g2y66m9g2mjn48hmk4", + "rewards": { + "rewards": [], + "period": "2" + } + } + ], + "delegator_starting_infos": [ + { + "delegator_address": "ethm1dakgyqjulg29m5fmv992g2y66m9g2mjn6hahwg", + "validator_address": "ethmvaloper1dakgyqjulg29m5fmv992g2y66m9g2mjn48hmk4", + "starting_info": { + "previous_period": "1", + "stake": "1000000.000000000000000000", + "height": "0" + } + } + ], + "validator_slash_events": [] + }, + "erc20": { + "params": { + "enable_erc20": true, + "native_precompiles": [ + "0xD4949664cD82660AaE99bEdc034a0deA8A0bd517" + ], + "dynamic_precompiles": [] + }, + "token_pairs": [ + { + "erc20_address": "0xD4949664cD82660AaE99bEdc034a0deA8A0bd517", + "denom": "token", + "enabled": true, + "contract_owner": "OWNER_MODULE", + "owner_address": "ethm1zrxl239wa6ad5xge3gs68rt98227xgnjq0xyw2" + } + ] + }, + "evidence": { + "evidence": [] + }, + "evm": { + "accounts": [ + { + "address": "0xD4949664cD82660AaE99bEdc034a0deA8A0bd517", + "code": "608060405234801561001057600080fd5b50600436106101da5760003560e01c80635c975abb11610104578063a217fddf116100a2578063d539139311610071578063d53913931461057d578063d547741f1461059b578063dd62ed3e146105b7578063e63ab1e9146105e7576101da565b8063a217fddf146104cf578063a457c2d7146104ed578063a9059cbb1461051d578063ca15c8731461054d576101da565b80638456cb59116100de5780638456cb59146104475780639010d07c1461045157806391d148541461048157806395d89b41146104b1576101da565b80635c975abb146103dd57806370a08231146103fb57806379cc67901461042b576101da565b8063282c51f31161017c578063395093511161014b578063395093511461036b5780633f4ba83a1461039b57806340c10f19146103a557806342966c68146103c1576101da565b8063282c51f3146102f75780632f2ff15d14610315578063313ce5671461033157806336568abe1461034f576101da565b806318160ddd116101b857806318160ddd1461025d5780631cf2c7e21461027b57806323b872dd14610297578063248a9ca3146102c7576101da565b806301ffc9a7146101df57806306fdde031461020f578063095ea7b31461022d575b600080fd5b6101f960048036038101906101f49190612216565b610605565b604051610206919061225e565b60405180910390f35b61021761067f565b6040516102249190612312565b60405180910390f35b610247600480360381019061024291906123c8565b610711565b604051610254919061225e565b60405180910390f35b61026561072f565b6040516102729190612417565b60405180910390f35b610295600480360381019061029091906123c8565b610739565b005b6102b160048036038101906102ac9190612432565b6107b7565b6040516102be919061225e565b60405180910390f35b6102e160048036038101906102dc91906124bb565b6108af565b6040516102ee91906124f7565b60405180910390f35b6102ff6108ce565b60405161030c91906124f7565b60405180910390f35b61032f600480360381019061032a9190612512565b6108f2565b005b61033961091b565b604051610346919061256e565b60405180910390f35b61036960048036038101906103649190612512565b610932565b005b610385600480360381019061038091906123c8565b6109b5565b604051610392919061225e565b60405180910390f35b6103a3610a61565b005b6103bf60048036038101906103ba91906123c8565b610adb565b005b6103db60048036038101906103d69190612589565b610b59565b005b6103e5610b6d565b6040516103f2919061225e565b60405180910390f35b610415600480360381019061041091906125b6565b610b84565b6040516104229190612417565b60405180910390f35b610445600480360381019061044091906123c8565b610bcd565b005b61044f610c48565b005b61046b600480360381019061046691906125e3565b610cc2565b6040516104789190612632565b60405180910390f35b61049b60048036038101906104969190612512565b610cf1565b6040516104a8919061225e565b60405180910390f35b6104b9610d5b565b6040516104c69190612312565b60405180910390f35b6104d7610ded565b6040516104e491906124f7565b60405180910390f35b610507600480360381019061050291906123c8565b610df4565b604051610514919061225e565b60405180910390f35b610537600480360381019061053291906123c8565b610edf565b604051610544919061225e565b60405180910390f35b610567600480360381019061056291906124bb565b610efd565b6040516105749190612417565b60405180910390f35b610585610f21565b60405161059291906124f7565b60405180910390f35b6105b560048036038101906105b09190612512565b610f45565b005b6105d160048036038101906105cc919061264d565b610f6e565b6040516105de9190612417565b60405180910390f35b6105ef610ff5565b6040516105fc91906124f7565b60405180910390f35b60007f5a05180f000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19161480610678575061067782611129565b5b9050919050565b60606005805461068e906126bc565b80601f01602080910402602001604051908101604052809291908181526020018280546106ba906126bc565b80156107075780601f106106dc57610100808354040283529160200191610707565b820191906000526020600020905b8154815290600101906020018083116106ea57829003601f168201915b5050505050905090565b600061072561071e6111a3565b84846111ab565b6001905092915050565b6000600454905090565b61076a7f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a8486107656111a3565b610cf1565b6107a9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107a090612760565b60405180910390fd5b6107b38282611376565b5050565b60006107c484848461154f565b6000600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600061080f6111a3565b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490508281101561088f576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610886906127f2565b60405180910390fd5b6108a38561089b6111a3565b8584036111ab565b60019150509392505050565b6000806000838152602001908152602001600020600101549050919050565b7f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a84881565b6108fb826108af565b61090c816109076111a3565b6117d3565b6109168383611870565b505050565b6000600760019054906101000a900460ff16905090565b61093a6111a3565b73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16146109a7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161099e90612884565b60405180910390fd5b6109b182826118a4565b5050565b6000610a576109c26111a3565b8484600360006109d06111a3565b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054610a5291906128d3565b6111ab565b6001905092915050565b610a927f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a610a8d6111a3565b610cf1565b610ad1576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610ac89061299b565b60405180910390fd5b610ad96118d8565b565b610b0c7f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6610b076111a3565b610cf1565b610b4b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b4290612a2d565b60405180910390fd5b610b55828261197a565b5050565b610b6a610b646111a3565b82611376565b50565b6000600760009054906101000a900460ff16905090565b6000600260008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6000610be083610bdb6111a3565b610f6e565b905081811015610c25576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610c1c90612abf565b60405180910390fd5b610c3983610c316111a3565b8484036111ab565b610c438383611376565b505050565b610c797f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a610c746111a3565b610cf1565b610cb8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610caf90612b51565b60405180910390fd5b610cc0611adb565b565b6000610ce98260016000868152602001908152602001600020611b7e90919063ffffffff16565b905092915050565b600080600084815260200190815260200160002060000160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16905092915050565b606060068054610d6a906126bc565b80601f0160208091040260200160405190810160405280929190818152602001828054610d96906126bc565b8015610de35780601f10610db857610100808354040283529160200191610de3565b820191906000526020600020905b815481529060010190602001808311610dc657829003601f168201915b5050505050905090565b6000801b81565b60008060036000610e036111a3565b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905082811015610ec0576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610eb790612be3565b60405180910390fd5b610ed4610ecb6111a3565b858584036111ab565b600191505092915050565b6000610ef3610eec6111a3565b848461154f565b6001905092915050565b6000610f1a60016000848152602001908152602001600020611b98565b9050919050565b7f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a681565b610f4e826108af565b610f5f81610f5a6111a3565b6117d3565b610f6983836118a4565b505050565b6000600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a81565b6110238282610cf1565b6110f557600160008084815260200190815260200160002060000160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff02191690831515021790555061109a6111a3565b73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45b5050565b6000611121836000018373ffffffffffffffffffffffffffffffffffffffff1660001b611bad565b905092915050565b60007f7965db0b000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916148061119c575061119b82611c1d565b5b9050919050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16141561121b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161121290612c75565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16141561128b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161128290612d07565b60405180910390fd5b80600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040516113699190612417565b60405180910390a3505050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614156113e6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016113dd90612d99565b60405180910390fd5b6113f282600083611c87565b6000600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905081811015611479576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161147090612e2b565b60405180910390fd5b818103600260008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555081600460008282546114d19190612e4b565b92505081905550600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516115369190612417565b60405180910390a361154a83600084611c97565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1614156115bf576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016115b690612ef1565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16141561162f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161162690612f83565b60405180910390fd5b61163a838383611c87565b6000600260008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156116c1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016116b890613015565b60405180910390fd5b818103600260008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555081600260008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461175691906128d3565b925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516117ba9190612417565b60405180910390a36117cd848484611c97565b50505050565b6117dd8282610cf1565b61186c576118028173ffffffffffffffffffffffffffffffffffffffff166014611c9c565b6118108360001c6020611c9c565b604051602001611821929190613109565b6040516020818303038152906040526040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016118639190612312565b60405180910390fd5b5050565b61187a8282611019565b61189f81600160008581526020019081526020016000206110f990919063ffffffff16565b505050565b6118ae8282611ed8565b6118d38160016000858152602001908152602001600020611fb990919063ffffffff16565b505050565b6118e0610b6d565b61191f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016119169061318f565b60405180910390fd5b6000600760006101000a81548160ff0219169083151502179055507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa6119636111a3565b6040516119709190612632565b60405180910390a1565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614156119ea576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016119e1906131fb565b60405180910390fd5b6119f660008383611c87565b8060046000828254611a0891906128d3565b9250508190555080600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254611a5e91906128d3565b925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051611ac39190612417565b60405180910390a3611ad760008383611c97565b5050565b611ae3610b6d565b15611b23576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611b1a90613267565b60405180910390fd5b6001600760006101000a81548160ff0219169083151502179055507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258611b676111a3565b604051611b749190612632565b60405180910390a1565b6000611b8d8360000183611fe9565b60001c905092915050565b6000611ba682600001612014565b9050919050565b6000611bb98383612025565b611c12578260000182908060018154018082558091505060019003906000526020600020016000909190919091505582600001805490508360010160008481526020019081526020016000208190555060019050611c17565b600090505b92915050565b60007f01ffc9a7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916149050919050565b611c92838383612048565b505050565b505050565b606060006002836002611caf9190613287565b611cb991906128d3565b67ffffffffffffffff811115611cd257611cd16132e1565b5b6040519080825280601f01601f191660200182016040528015611d045781602001600182028036833780820191505090505b5090507f300000000000000000000000000000000000000000000000000000000000000081600081518110611d3c57611d3b613310565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053507f780000000000000000000000000000000000000000000000000000000000000081600181518110611da057611d9f613310565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535060006001846002611de09190613287565b611dea91906128d3565b90505b6001811115611e8a577f3031323334353637383961626364656600000000000000000000000000000000600f861660108110611e2c57611e2b613310565b5b1a60f81b828281518110611e4357611e42613310565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a905350600485901c945080611e839061333f565b9050611ded565b5060008414611ece576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611ec5906133b5565b60405180910390fd5b8091505092915050565b611ee28282610cf1565b15611fb557600080600084815260200190815260200160002060000160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff021916908315150217905550611f5a6111a3565b73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16837ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b60405160405180910390a45b5050565b6000611fe1836000018373ffffffffffffffffffffffffffffffffffffffff1660001b6120a0565b905092915050565b600082600001828154811061200157612000613310565b5b9060005260206000200154905092915050565b600081600001805490509050919050565b600080836001016000848152602001908152602001600020541415905092915050565b6120538383836121b4565b61205b610b6d565b1561209b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161209290613447565b60405180910390fd5b505050565b600080836001016000848152602001908152602001600020549050600081146121a85760006001826120d29190612e4b565b90506000600186600001805490506120ea9190612e4b565b905081811461215957600086600001828154811061210b5761210a613310565b5b906000526020600020015490508087600001848154811061212f5761212e613310565b5b90600052602060002001819055508387600101600083815260200190815260200160002081905550505b8560000180548061216d5761216c613467565b5b6001900381819060005260206000200160009055905585600101600086815260200190815260200160002060009055600193505050506121ae565b60009150505b92915050565b505050565b600080fd5b60007fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b6121f3816121be565b81146121fe57600080fd5b50565b600081359050612210816121ea565b92915050565b60006020828403121561222c5761222b6121b9565b5b600061223a84828501612201565b91505092915050565b60008115159050919050565b61225881612243565b82525050565b6000602082019050612273600083018461224f565b92915050565b600081519050919050565b600082825260208201905092915050565b60005b838110156122b3578082015181840152602081019050612298565b838111156122c2576000848401525b50505050565b6000601f19601f8301169050919050565b60006122e482612279565b6122ee8185612284565b93506122fe818560208601612295565b612307816122c8565b840191505092915050565b6000602082019050818103600083015261232c81846122d9565b905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061235f82612334565b9050919050565b61236f81612354565b811461237a57600080fd5b50565b60008135905061238c81612366565b92915050565b6000819050919050565b6123a581612392565b81146123b057600080fd5b50565b6000813590506123c28161239c565b92915050565b600080604083850312156123df576123de6121b9565b5b60006123ed8582860161237d565b92505060206123fe858286016123b3565b9150509250929050565b61241181612392565b82525050565b600060208201905061242c6000830184612408565b92915050565b60008060006060848603121561244b5761244a6121b9565b5b60006124598682870161237d565b935050602061246a8682870161237d565b925050604061247b868287016123b3565b9150509250925092565b6000819050919050565b61249881612485565b81146124a357600080fd5b50565b6000813590506124b58161248f565b92915050565b6000602082840312156124d1576124d06121b9565b5b60006124df848285016124a6565b91505092915050565b6124f181612485565b82525050565b600060208201905061250c60008301846124e8565b92915050565b60008060408385031215612529576125286121b9565b5b6000612537858286016124a6565b92505060206125488582860161237d565b9150509250929050565b600060ff82169050919050565b61256881612552565b82525050565b6000602082019050612583600083018461255f565b92915050565b60006020828403121561259f5761259e6121b9565b5b60006125ad848285016123b3565b91505092915050565b6000602082840312156125cc576125cb6121b9565b5b60006125da8482850161237d565b91505092915050565b600080604083850312156125fa576125f96121b9565b5b6000612608858286016124a6565b9250506020612619858286016123b3565b9150509250929050565b61262c81612354565b82525050565b60006020820190506126476000830184612623565b92915050565b60008060408385031215612664576126636121b9565b5b60006126728582860161237d565b92505060206126838582860161237d565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806126d457607f821691505b602082108114156126e8576126e761268d565b5b50919050565b7f45524332304d696e7465724275726e6572446563696d616c733a206d7573742060008201527f68617665206275726e657220726f6c6520746f206275726e0000000000000000602082015250565b600061274a603883612284565b9150612755826126ee565b604082019050919050565b600060208201905081810360008301526127798161273d565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206160008201527f6c6c6f77616e6365000000000000000000000000000000000000000000000000602082015250565b60006127dc602883612284565b91506127e782612780565b604082019050919050565b6000602082019050818103600083015261280b816127cf565b9050919050565b7f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560008201527f20726f6c657320666f722073656c660000000000000000000000000000000000602082015250565b600061286e602f83612284565b915061287982612812565b604082019050919050565b6000602082019050818103600083015261289d81612861565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006128de82612392565b91506128e983612392565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0382111561291e5761291d6128a4565b5b828201905092915050565b7f45524332304d696e7465724275726e6572446563696d616c733a206d7573742060008201527f686176652070617573657220726f6c6520746f20756e70617573650000000000602082015250565b6000612985603b83612284565b915061299082612929565b604082019050919050565b600060208201905081810360008301526129b481612978565b9050919050565b7f45524332304d696e7465724275726e6572446563696d616c733a206d7573742060008201527f68617665206d696e74657220726f6c6520746f206d696e740000000000000000602082015250565b6000612a17603883612284565b9150612a22826129bb565b604082019050919050565b60006020820190508181036000830152612a4681612a0a565b9050919050565b7f45524332303a206275726e20616d6f756e74206578636565647320616c6c6f7760008201527f616e636500000000000000000000000000000000000000000000000000000000602082015250565b6000612aa9602483612284565b9150612ab482612a4d565b604082019050919050565b60006020820190508181036000830152612ad881612a9c565b9050919050565b7f45524332304d696e7465724275726e6572446563696d616c733a206d7573742060008201527f686176652070617573657220726f6c6520746f20706175736500000000000000602082015250565b6000612b3b603983612284565b9150612b4682612adf565b604082019050919050565b60006020820190508181036000830152612b6a81612b2e565b9050919050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000612bcd602583612284565b9150612bd882612b71565b604082019050919050565b60006020820190508181036000830152612bfc81612bc0565b9050919050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b6000612c5f602483612284565b9150612c6a82612c03565b604082019050919050565b60006020820190508181036000830152612c8e81612c52565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b6000612cf1602283612284565b9150612cfc82612c95565b604082019050919050565b60006020820190508181036000830152612d2081612ce4565b9050919050565b7f45524332303a206275726e2066726f6d20746865207a65726f2061646472657360008201527f7300000000000000000000000000000000000000000000000000000000000000602082015250565b6000612d83602183612284565b9150612d8e82612d27565b604082019050919050565b60006020820190508181036000830152612db281612d76565b9050919050565b7f45524332303a206275726e20616d6f756e7420657863656564732062616c616e60008201527f6365000000000000000000000000000000000000000000000000000000000000602082015250565b6000612e15602283612284565b9150612e2082612db9565b604082019050919050565b60006020820190508181036000830152612e4481612e08565b9050919050565b6000612e5682612392565b9150612e6183612392565b925082821015612e7457612e736128a4565b5b828203905092915050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b6000612edb602583612284565b9150612ee682612e7f565b604082019050919050565b60006020820190508181036000830152612f0a81612ece565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b6000612f6d602383612284565b9150612f7882612f11565b604082019050919050565b60006020820190508181036000830152612f9c81612f60565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b6000612fff602683612284565b915061300a82612fa3565b604082019050919050565b6000602082019050818103600083015261302e81612ff2565b9050919050565b600081905092915050565b7f416363657373436f6e74726f6c3a206163636f756e7420000000000000000000600082015250565b6000613076601783613035565b915061308182613040565b601782019050919050565b600061309782612279565b6130a18185613035565b93506130b1818560208601612295565b80840191505092915050565b7f206973206d697373696e6720726f6c6520000000000000000000000000000000600082015250565b60006130f3601183613035565b91506130fe826130bd565b601182019050919050565b600061311482613069565b9150613120828561308c565b915061312b826130e6565b9150613137828461308c565b91508190509392505050565b7f5061757361626c653a206e6f7420706175736564000000000000000000000000600082015250565b6000613179601483612284565b915061318482613143565b602082019050919050565b600060208201905081810360008301526131a88161316c565b9050919050565b7f45524332303a206d696e7420746f20746865207a65726f206164647265737300600082015250565b60006131e5601f83612284565b91506131f0826131af565b602082019050919050565b60006020820190508181036000830152613214816131d8565b9050919050565b7f5061757361626c653a2070617573656400000000000000000000000000000000600082015250565b6000613251601083612284565b915061325c8261321b565b602082019050919050565b6000602082019050818103600083015261328081613244565b9050919050565b600061329282612392565b915061329d83612392565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04831182151516156132d6576132d56128a4565b5b828202905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600061334a82612392565b9150600082141561335e5761335d6128a4565b5b600182039050919050565b7f537472696e67733a20686578206c656e67746820696e73756666696369656e74600082015250565b600061339f602083612284565b91506133aa82613369565b602082019050919050565b600060208201905081810360008301526133ce81613392565b9050919050565b7f45524332305061757361626c653a20746f6b656e207472616e7366657220776860008201527f696c652070617573656400000000000000000000000000000000000000000000602082015250565b6000613431602a83612284565b915061343c826133d5565b604082019050919050565b6000602082019050818103600083015261346081613424565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fdfea2646970667358221220c3d4a4231a6c94cfb03623ea4b77df2c9ccfa487132bebf43620219e3dc2f4cf64736f6c63430008090033", + "storage": [] + } + ], + "params": { + "evm_denom": "token", + "extra_eips": [ + "ethereum_3855" + ], + "chain_config": { + "homestead_block": "0", + "dao_fork_block": "0", + "dao_fork_support": true, + "eip150_block": "0", + "eip150_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155_block": "0", + "eip158_block": "0", + "byzantium_block": "0", + "constantinople_block": "0", + "petersburg_block": "0", + "istanbul_block": "0", + "muir_glacier_block": "0", + "berlin_block": "0", + "london_block": "0", + "arrow_glacier_block": "0", + "gray_glacier_block": "0", + "merge_netsplit_block": "0", + "shanghai_block": "0", + "cancun_block": "0" + }, + "allow_unprotected_txs": true, + "evm_channels": [ + "channel-10", + "channel-31", + "channel-83" + ], + "access_control": { + "create": { + "access_type": "ACCESS_TYPE_PERMISSIONLESS", + "access_control_list": [] + }, + "call": { + "access_type": "ACCESS_TYPE_PERMISSIONLESS", + "access_control_list": [] + } + }, + "active_static_precompiles": [ + "0x0000000000000000000000000000000000000100", + "0x0000000000000000000000000000000000000400", + "0x0000000000000000000000000000000000000800", + "0x0000000000000000000000000000000000000801", + "0x0000000000000000000000000000000000000802", + "0x0000000000000000000000000000000000000803", + "0x0000000000000000000000000000000000000804", + "0x0000000000000000000000000000000000000805" + ] + } + }, + "feegrant": { + "allowances": [] + }, + "feemarket": { + "params": { + "no_base_fee": false, + "base_fee_change_denominator": 8, + "elasticity_multiplier": 2, + "enable_height": "0", + "base_fee": "0", + "min_gas_price": "0.000000000000000000", + "min_gas_multiplier": "0.500000000000000000" + }, + "block_gas": "0" + }, + "genutil": { + "gen_txs": [] + }, + "gov": { + "starting_proposal_id": "1", + "deposits": [], + "votes": [], + "proposals": [], + "deposit_params": null, + "voting_params": null, + "tally_params": null, + "params": { + "min_deposit": [ + { + "denom": "token", + "amount": "1" + } + ], + "max_deposit_period": "172800s", + "voting_period": "10s", + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto_threshold": "0.334000000000000000", + "min_initial_deposit_ratio": "0.000000000000000000", + "proposal_cancel_ratio": "0.500000000000000000", + "proposal_cancel_dest": "", + "expedited_voting_period": "5s", + "expedited_threshold": "0.667000000000000000", + "expedited_min_deposit": [ + { + "denom": "stake", + "amount": "50000000" + } + ], + "burn_vote_quorum": false, + "burn_proposal_deposit_prevote": false, + "burn_vote_veto": true, + "min_deposit_ratio": "0.010000000000000000" + }, + "constitution": "" + }, + "ibc": { + "client_genesis": { + "clients": [ + { + "client_id": "09-localhost", + "client_state": { + "@type": "/ibc.lightclients.localhost.v2.ClientState", + "latest_height": { + "revision_number": "1", + "revision_height": "7" + } + } + } + ], + "clients_consensus": [], + "clients_metadata": [], + "params": { + "allowed_clients": [ + "*" + ] + }, + "create_localhost": false, + "next_client_sequence": "0" + }, + "connection_genesis": { + "connections": [ + { + "id": "connection-localhost", + "client_id": "09-localhost", + "versions": [ + { + "identifier": "1", + "features": [ + "ORDER_ORDERED", + "ORDER_UNORDERED" + ] + } + ], + "state": "STATE_OPEN", + "counterparty": { + "client_id": "09-localhost", + "connection_id": "connection-localhost", + "prefix": { + "key_prefix": "aWJj" + } + }, + "delay_period": "0" + } + ], + "client_connection_paths": [], + "next_connection_sequence": "0", + "params": { + "max_expected_time_per_block": "30000000000" + } + }, + "channel_genesis": { + "channels": [], + "acknowledgements": [], + "commitments": [], + "receipts": [], + "send_sequences": [], + "recv_sequences": [], + "ack_sequences": [], + "next_channel_sequence": "0", + "params": { + "upgrade_timeout": { + "height": { + "revision_number": "0", + "revision_height": "0" + }, + "timestamp": "600000000000" + } + } + } + }, + "interchainaccounts": { + "controller_genesis_state": { + "active_channels": [], + "interchain_accounts": [], + "ports": [], + "params": { + "controller_enabled": true + } + }, + "host_genesis_state": { + "active_channels": [], + "interchain_accounts": [], + "port": "icahost", + "params": { + "host_enabled": true, + "allow_messages": [ + "*" + ] + } + } + }, + "poa": { + "params": {} + }, + "ratelimit": { + "params": {}, + "rate_limits": [], + "whitelisted_address_pairs": [], + "blacklisted_denoms": [], + "pending_send_packet_sequence_numbers": [], + "hour_epoch": { + "epoch_number": "17", + "duration": "3600s", + "epoch_start_time": "2024-11-27T17:00:00Z", + "epoch_start_height": "0" + } + }, + "slashing": { + "params": { + "signed_blocks_window": "100", + "min_signed_per_window": "0.500000000000000000", + "downtime_jail_duration": "600s", + "slash_fraction_double_sign": "0.000000000000000000", + "slash_fraction_downtime": "0.000000000000000000" + }, + "signing_infos": [ + { + "address": "ethmvalcons123s7tvluhrla7tpj5mwysxqlcgwwzwavxenru7", + "validator_signing_info": { + "address": "ethmvalcons123s7tvluhrla7tpj5mwysxqlcgwwzwavxenru7", + "start_height": "0", + "index_offset": "6", + "jailed_until": "1970-01-01T00:00:00Z", + "tombstoned": false, + "missed_blocks_counter": "0" + } + } + ], + "missed_blocks": [ + { + "address": "ethmvalcons123s7tvluhrla7tpj5mwysxqlcgwwzwavxenru7", + "missed_blocks": [] + } + ] + }, + "staking": { + "params": { + "unbonding_time": "60s", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 10000, + "bond_denom": "apoa", + "min_commission_rate": "0.000000000000000000" + }, + "last_total_power": "1", + "last_validator_powers": [ + { + "address": "ethmvaloper1dakgyqjulg29m5fmv992g2y66m9g2mjn48hmk4", + "power": "1" + } + ], + "validators": [ + { + "operator_address": "ethmvaloper1dakgyqjulg29m5fmv992g2y66m9g2mjn48hmk4", + "consensus_pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "x39TSSHGypcFLoD91nkVBQTiRT/KcVnnvyJrnXn0Bps=" + }, + "jailed": false, + "status": "BOND_STATUS_BONDED", + "tokens": "1000000", + "delegator_shares": "1000000.000000000000000000", + "description": { + "moniker": "localnet", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "unbonding_height": "0", + "unbonding_time": "1970-01-01T00:00:00Z", + "commission": { + "commission_rates": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "update_time": "2024-11-27T17:14:04.871328Z" + }, + "min_self_delegation": "1", + "unbonding_on_hold_ref_count": "0", + "unbonding_ids": [] + } + ], + "delegations": [ + { + "delegator_address": "ethm1dakgyqjulg29m5fmv992g2y66m9g2mjn6hahwg", + "validator_address": "ethmvaloper1dakgyqjulg29m5fmv992g2y66m9g2mjn48hmk4", + "shares": "1000000.000000000000000000" + } + ], + "unbonding_delegations": [], + "redelegations": [], + "exported": true + }, + "transfer": { + "port_id": "transfer", + "denom_traces": [], + "params": { + "send_enabled": true, + "receive_enabled": true + }, + "total_escrowed": [] + }, + "upgrade": {} + }, + "consensus": { + "validators": [ + { + "address": "5461E5B3FCB8FFDF2C32A6DC48181FC21CE13BAC", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "x39TSSHGypcFLoD91nkVBQTiRT/KcVnnvyJrnXn0Bps=" + }, + "power": "1", + "name": "localnet" + } + ], + "params": { + "block": { + "max_bytes": 22020096, + "max_gas": 10500000 + }, + "evidence": { + "max_age_num_blocks": 100000, + "max_age_duration": 172800000000000, + "max_bytes": 1048576 + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + }, + "version": { + "app": 0 + }, + "abci": { + "vote_extensions_enable_height": 0 + } + } + } +} \ No newline at end of file diff --git a/testutil/integration/common/factory/base.go b/testutil/integration/common/factory/base.go new file mode 100644 index 0000000..7239e67 --- /dev/null +++ b/testutil/integration/common/factory/base.go @@ -0,0 +1,118 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package factory + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + abcitypes "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/client" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" + testutiltypes "github.com/cosmos/cosmos-sdk/types/module/testutil" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/xrplevm/node/v6/testutil/integration/common/grpc" + "github.com/xrplevm/node/v6/testutil/integration/common/network" +) + +// BaseTxFactory is the interface that wraps the common methods to build and broadcast transactions +// within cosmos chains +type BaseTxFactory interface { + // BuildCosmosTx builds a Cosmos tx with the provided private key and txArgs + BuildCosmosTx(privKey cryptotypes.PrivKey, txArgs CosmosTxArgs) (authsigning.Tx, error) + // SignCosmosTx signs a Cosmos transaction with the provided + // private key and tx builder + SignCosmosTx(privKey cryptotypes.PrivKey, txBuilder client.TxBuilder) error + // ExecuteCosmosTx builds, signs and broadcasts a Cosmos tx with the provided private key and txArgs + ExecuteCosmosTx(privKey cryptotypes.PrivKey, txArgs CosmosTxArgs) (abcitypes.ExecTxResult, error) + // EncodeTx encodes the provided transaction + EncodeTx(tx sdktypes.Tx) ([]byte, error) + // CommitCosmosTx creates, signs and commits a cosmos tx + // (produces a block with the specified transaction) + CommitCosmosTx(privKey cryptotypes.PrivKey, txArgs CosmosTxArgs) (abcitypes.ExecTxResult, error) +} + +// baseTxFactory is the struct of the basic tx factory +// to build and broadcast transactions. +// This is to simulate the behavior of a real user. +type baseTxFactory struct { + grpcHandler grpc.Handler + network network.Network + ec testutiltypes.TestEncodingConfig +} + +// newBaseTxFactory instantiates a new baseTxFactory +func newBaseTxFactory( + network network.Network, + grpcHandler grpc.Handler, +) BaseTxFactory { + return &baseTxFactory{ + grpcHandler: grpcHandler, + network: network, + ec: network.GetEncodingConfig(), + } +} + +func (tf *baseTxFactory) BuildCosmosTx(privKey cryptotypes.PrivKey, txArgs CosmosTxArgs) (authsigning.Tx, error) { + txBuilder, err := tf.buildTx(privKey, txArgs) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to build tx") + } + return txBuilder.GetTx(), nil +} + +// ExecuteCosmosTx creates, signs and broadcasts a Cosmos transaction +func (tf *baseTxFactory) ExecuteCosmosTx(privKey cryptotypes.PrivKey, txArgs CosmosTxArgs) (abcitypes.ExecTxResult, error) { + signedTx, err := tf.BuildCosmosTx(privKey, txArgs) + if err != nil { + return abcitypes.ExecTxResult{}, errorsmod.Wrap(err, "failed to build tx") + } + + txBytes, err := tf.EncodeTx(signedTx) + if err != nil { + return abcitypes.ExecTxResult{}, errorsmod.Wrap(err, "failed to encode tx") + } + + return tf.network.BroadcastTxSync(txBytes) +} + +// CommitCosmosTx creates and signs a Cosmos transaction, and then includes it in +// a block and commits the state changes on the chain +func (tf *baseTxFactory) CommitCosmosTx(privKey cryptotypes.PrivKey, txArgs CosmosTxArgs) (abcitypes.ExecTxResult, error) { + signedTx, err := tf.BuildCosmosTx(privKey, txArgs) + if err != nil { + return abcitypes.ExecTxResult{}, errorsmod.Wrap(err, "failed to build tx") + } + + txBytes, err := tf.EncodeTx(signedTx) + if err != nil { + return abcitypes.ExecTxResult{}, errorsmod.Wrap(err, "failed to encode tx") + } + + blockRes, err := tf.network.NextBlockWithTxs(txBytes) + if err != nil { + return abcitypes.ExecTxResult{}, errorsmod.Wrap(err, "failed to include the tx in a block") + } + txResCount := len(blockRes.TxResults) + if txResCount != 1 { + return abcitypes.ExecTxResult{}, fmt.Errorf("expected to receive only one tx result, but got %d", txResCount) + } + return *blockRes.TxResults[0], nil +} + +// SignCosmosTx is a helper function that signs a Cosmos transaction +// with the provided private key and transaction builder +func (tf *baseTxFactory) SignCosmosTx(privKey cryptotypes.PrivKey, txBuilder client.TxBuilder) error { + txConfig := tf.ec.TxConfig + signMode, err := authsigning.APISignModeToInternal(txConfig.SignModeHandler().DefaultMode()) + if err != nil { + return errorsmod.Wrap(err, "invalid sign mode") + } + signerData, err := tf.setSignatures(privKey, txBuilder, signMode) + if err != nil { + return errorsmod.Wrap(err, "failed to set tx signatures") + } + + return tf.signWithPrivKey(privKey, txBuilder, signerData, signMode) +} diff --git a/testutil/integration/common/factory/distribution.go b/testutil/integration/common/factory/distribution.go new file mode 100644 index 0000000..ed530a6 --- /dev/null +++ b/testutil/integration/common/factory/distribution.go @@ -0,0 +1,87 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package factory + +import ( + "fmt" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" +) + +type DistributionTxFactory interface { + // SetWithdrawAddress is a method to create and broadcast a MsgSetWithdrawAddress + SetWithdrawAddress(delegatorPriv cryptotypes.PrivKey, withdrawerAddr sdk.AccAddress) error + // WithdrawDelegationRewards is a method to create and broadcast a MsgWithdrawDelegationRewards + WithdrawDelegationRewards(delegatorPriv cryptotypes.PrivKey, validatorAddr string) error + // WithdrawValidatorCommission is a method to create and broadcast a MsgWithdrawValidatorCommission + WithdrawValidatorCommission(validatorPriv cryptotypes.PrivKey) error +} + +type distributionTxFactory struct { + BaseTxFactory +} + +func newDistrTxFactory(bf BaseTxFactory) DistributionTxFactory { + return &distributionTxFactory{bf} +} + +func (tf *distributionTxFactory) SetWithdrawAddress(delegatorPriv cryptotypes.PrivKey, withdrawerAddr sdk.AccAddress) error { + delegatorAccAddr := sdk.AccAddress(delegatorPriv.PubKey().Address()) + + msg := distrtypes.NewMsgSetWithdrawAddress( + delegatorAccAddr, + withdrawerAddr, + ) + + resp, err := tf.ExecuteCosmosTx(delegatorPriv, CosmosTxArgs{ + Msgs: []sdk.Msg{msg}, + }) + + if resp.Code != 0 { + err = fmt.Errorf("received error code %d on SetWithdrawAddress transaction. Logs: %s", resp.Code, resp.Log) + } + + return err +} + +// WithdrawDelegationRewards will withdraw any unclaimed staking rewards for the delegator associated with +// the given private key from the validator. +// The validator address should be in the format `evmosvaloper1...`. +func (tf *distributionTxFactory) WithdrawDelegationRewards(delegatorPriv cryptotypes.PrivKey, validatorAddr string) error { + delegatorAccAddr := sdk.AccAddress(delegatorPriv.PubKey().Address()) + + msg := distrtypes.NewMsgWithdrawDelegatorReward( + delegatorAccAddr.String(), + validatorAddr, + ) + + resp, err := tf.ExecuteCosmosTx(delegatorPriv, CosmosTxArgs{ + Msgs: []sdk.Msg{msg}, + }) + + if resp.Code != 0 { + err = fmt.Errorf("received error code %d on WithdrawDelegationRewards transaction. Logs: %s", resp.Code, resp.Log) + } + + return err +} + +func (tf *distributionTxFactory) WithdrawValidatorCommission(validatorPriv cryptotypes.PrivKey) error { + validatorAddr := sdk.ValAddress(validatorPriv.PubKey().Address()) + + msg := distrtypes.NewMsgWithdrawValidatorCommission( + validatorAddr.String(), + ) + + resp, err := tf.ExecuteCosmosTx(validatorPriv, CosmosTxArgs{ + Msgs: []sdk.Msg{msg}, + }) + + if resp.Code != 0 { + err = fmt.Errorf("received error code %d on WithdrawValidatorCommission transaction. Logs: %s", resp.Code, resp.Log) + } + + return err +} diff --git a/testutil/integration/common/factory/factory.go b/testutil/integration/common/factory/factory.go new file mode 100644 index 0000000..c07d19f --- /dev/null +++ b/testutil/integration/common/factory/factory.go @@ -0,0 +1,48 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package factory + +import ( + "github.com/xrplevm/node/v6/testutil/integration/common/grpc" + "github.com/xrplevm/node/v6/testutil/integration/common/network" +) + +const ( + GasAdjustment = float64(1.7) +) + +// CoreTxFactory is the interface that wraps the methods +// to build and broadcast cosmos transactions, and also +// includes module-specific transactions +type CoreTxFactory interface { + BaseTxFactory + DistributionTxFactory + StakingTxFactory + FundTxFactory +} + +var _ CoreTxFactory = (*IntegrationTxFactory)(nil) + +// IntegrationTxFactory is a helper struct to build and broadcast transactions +// to the network on integration tests. This is to simulate the behavior of a real user. +type IntegrationTxFactory struct { + BaseTxFactory + DistributionTxFactory + StakingTxFactory + FundTxFactory +} + +// New creates a new IntegrationTxFactory instance +func New( + network network.Network, + grpcHandler grpc.Handler, +) CoreTxFactory { + bf := newBaseTxFactory(network, grpcHandler) + return &IntegrationTxFactory{ + bf, + newDistrTxFactory(bf), + newStakingTxFactory(bf), + newFundTxFactory(bf), + } +} diff --git a/testutil/integration/common/factory/fund.go b/testutil/integration/common/factory/fund.go new file mode 100644 index 0000000..b7aa778 --- /dev/null +++ b/testutil/integration/common/factory/fund.go @@ -0,0 +1,51 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package factory + +import ( + "fmt" + + sdktypes "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/xrplevm/node/v6/testutil/integration/common/keyring" +) + +// FundTxFactory is the interface that wraps the common methods to fund accounts +// via a bank send transaction +type FundTxFactory interface { + // FundAccount funds the given account with the given amount. + FundAccount(sender keyring.Key, receiver sdktypes.AccAddress, amount sdktypes.Coins) error +} + +// baseTxFactory is the struct of the basic tx factory +// to build and broadcast transactions. +// This is to simulate the behavior of a real user. +type fundTxFactory struct { + BaseTxFactory +} + +// newBaseTxFactory instantiates a new baseTxFactory +func newFundTxFactory(bf BaseTxFactory) FundTxFactory { + return &fundTxFactory{bf} +} + +// FundAccount funds the given account with the given amount of coins. +func (tf *fundTxFactory) FundAccount(sender keyring.Key, receiver sdktypes.AccAddress, coins sdktypes.Coins) error { + bankmsg := banktypes.NewMsgSend( + sender.AccAddr, + receiver, + coins, + ) + txArgs := CosmosTxArgs{Msgs: []sdktypes.Msg{bankmsg}} + txRes, err := tf.ExecuteCosmosTx(sender.Priv, txArgs) + if err != nil { + return err + } + + if txRes.Code != 0 { + return fmt.Errorf("transaction returned non-zero code %d", txRes.Code) + } + + return nil +} diff --git a/testutil/integration/common/factory/helper.go b/testutil/integration/common/factory/helper.go new file mode 100644 index 0000000..6950bd0 --- /dev/null +++ b/testutil/integration/common/factory/helper.go @@ -0,0 +1,119 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package factory + +import ( + "math/big" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" +) + +// EncodeTx encodes the tx using the txConfig's encoder. +func (tf *baseTxFactory) EncodeTx(tx sdktypes.Tx) ([]byte, error) { + txConfig := tf.ec.TxConfig + txBytes, err := txConfig.TxEncoder()(tx) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to encode tx") + } + return txBytes, nil +} + +// buildTx builds a tx with the provided private key and txArgs +func (tf *baseTxFactory) buildTx(privKey cryptotypes.PrivKey, txArgs CosmosTxArgs) (client.TxBuilder, error) { + txConfig := tf.ec.TxConfig + txBuilder := txConfig.NewTxBuilder() + + if err := txBuilder.SetMsgs(txArgs.Msgs...); err != nil { + return nil, errorsmod.Wrap(err, "failed to set tx msgs") + } + + if txArgs.FeeGranter != nil { + txBuilder.SetFeeGranter(txArgs.FeeGranter) + } + + senderAddress := sdktypes.AccAddress(privKey.PubKey().Address().Bytes()) + + if txArgs.FeeGranter != nil { + txBuilder.SetFeeGranter(txArgs.FeeGranter) + } + + txBuilder.SetFeePayer(senderAddress) + + // need to sign the tx to simulate the tx to get the gas estimation + signMode, err := authsigning.APISignModeToInternal(txConfig.SignModeHandler().DefaultMode()) + if err != nil { + return nil, errorsmod.Wrap(err, "invalid sign mode") + } + signerData, err := tf.setSignatures(privKey, txBuilder, signMode) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to set tx signatures") + } + + gasLimit, err := tf.estimateGas(txArgs, txBuilder) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to estimate gas") + } + txBuilder.SetGasLimit(gasLimit) + + fees := txArgs.Fees + if fees.IsZero() { + fees, err = tf.calculateFees(txArgs.GasPrice, gasLimit) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to calculate fees") + } + } + txBuilder.SetFeeAmount(fees) + + if err := tf.signWithPrivKey(privKey, txBuilder, signerData, signMode); err != nil { + return nil, errorsmod.Wrap(err, "failed to sign Cosmos Tx") + } + + return txBuilder, nil +} + +// calculateFees calculates the fees for the transaction. +func (tf *baseTxFactory) calculateFees(gasPrice *sdkmath.Int, gasLimit uint64) (sdktypes.Coins, error) { + denom := tf.network.GetDenom() + var fees sdktypes.Coins + if gasPrice != nil { + fees = sdktypes.Coins{{Denom: denom, Amount: gasPrice.MulRaw(int64(gasLimit))}} //#nosec G115 + } else { + resp, err := tf.grpcHandler.GetBaseFee() + if err != nil { + return sdktypes.Coins{}, errorsmod.Wrap(err, "failed to get base fee") + } + price := resp.BaseFee + fees = sdktypes.Coins{{Denom: denom, Amount: price.MulRaw(int64(gasLimit))}} //#nosec G115 + } + return fees, nil +} + +// estimateGas estimates the gas needed for the transaction. +func (tf *baseTxFactory) estimateGas(txArgs CosmosTxArgs, txBuilder client.TxBuilder) (uint64, error) { + txConfig := tf.ec.TxConfig + simulateBytes, err := txConfig.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return 0, errorsmod.Wrap(err, "failed to encode tx") + } + + var gasLimit uint64 + if txArgs.Gas == nil { + simulateRes, err := tf.network.Simulate(simulateBytes) + if err != nil { + return 0, errorsmod.Wrap(err, "failed to simulate tx") + } + + gasAdj := new(big.Float).SetFloat64(GasAdjustment) + gasUsed := new(big.Float).SetUint64(simulateRes.GasInfo.GasUsed) + gasLimit, _ = gasAdj.Mul(gasAdj, gasUsed).Uint64() + } else { + gasLimit = *txArgs.Gas + } + return gasLimit, nil +} diff --git a/testutil/integration/common/factory/sign.go b/testutil/integration/common/factory/sign.go new file mode 100644 index 0000000..6686dfe --- /dev/null +++ b/testutil/integration/common/factory/sign.go @@ -0,0 +1,58 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package factory + +import ( + "context" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/client" + cosmostx "github.com/cosmos/cosmos-sdk/client/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" +) + +// setSignatures is a helper function that sets the signature for +// the transaction in the tx builder. It returns the signerData to be used +// when signing the transaction (e.g. when calling signWithPrivKey) +func (tf *baseTxFactory) setSignatures(privKey cryptotypes.PrivKey, txBuilder client.TxBuilder, signMode signing.SignMode) (signerData authsigning.SignerData, err error) { + senderAddress := sdktypes.AccAddress(privKey.PubKey().Address().Bytes()) + account, err := tf.grpcHandler.GetAccount(senderAddress.String()) + if err != nil { + return signerData, err + } + sequence := account.GetSequence() + signerData = authsigning.SignerData{ + ChainID: tf.network.GetChainID(), + AccountNumber: account.GetAccountNumber(), + Sequence: sequence, + Address: senderAddress.String(), + PubKey: privKey.PubKey(), + } + + sigsV2 := signing.SignatureV2{ + PubKey: privKey.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: signMode, + Signature: nil, + }, + Sequence: sequence, + } + + return signerData, txBuilder.SetSignatures(sigsV2) +} + +// signWithPrivKey is a helper function that signs a transaction +// with the provided private key +func (tf *baseTxFactory) signWithPrivKey(privKey cryptotypes.PrivKey, txBuilder client.TxBuilder, signerData authsigning.SignerData, signMode signing.SignMode) error { + txConfig := tf.ec.TxConfig + signature, err := cosmostx.SignWithPrivKey(context.TODO(), signMode, signerData, txBuilder, privKey, txConfig, signerData.Sequence) + if err != nil { + return errorsmod.Wrap(err, "failed to sign tx") + } + + return txBuilder.SetSignatures(signature) +} diff --git a/testutil/integration/common/factory/staking.go b/testutil/integration/common/factory/staking.go new file mode 100644 index 0000000..72799a4 --- /dev/null +++ b/testutil/integration/common/factory/staking.go @@ -0,0 +1,88 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package factory + +import ( + "fmt" + + "cosmossdk.io/math" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +type StakingTxFactory interface { + // Delegate is a method to create and execute a MsgDelegate paying always the same fee amount + // The tx is included in a block and committed in the chain state + Delegate(delegatorPriv cryptotypes.PrivKey, validatorAddr string, amount sdk.Coin) error + // CreateValidator is a method to create and broadcast a MsgCreateValidator + CreateValidator(operatorPriv cryptotypes.PrivKey, pubKey cryptotypes.PubKey, selfDelegation sdk.Coin, description stakingtypes.Description, commission stakingtypes.CommissionRates, minSelfDelegation math.Int) error +} + +type stakingTxFactory struct { + BaseTxFactory +} + +func newStakingTxFactory(bf BaseTxFactory) StakingTxFactory { + return &stakingTxFactory{bf} +} + +// Delegate on behalf of the account associated with the given private key. +// The defined amount will delegated to the specified validator. +// The validator address should be in the format `evmosvaloper1...`. +func (tf *stakingTxFactory) Delegate(delegatorPriv cryptotypes.PrivKey, validatorAddr string, amount sdk.Coin) error { + delegatorAccAddr := sdk.AccAddress(delegatorPriv.PubKey().Address()) + + msgDelegate := stakingtypes.NewMsgDelegate( + delegatorAccAddr.String(), + validatorAddr, + amount, + ) + + // set gas and gas prices to pay the same fees + // every time this function is called + feesToPay := math.NewInt(1e16) + gas := uint64(400_000) + gasPrice := feesToPay.QuoRaw(int64(gas)) //#nosec G115 -- gas will not exceed int64 + + res, err := tf.CommitCosmosTx(delegatorPriv, CosmosTxArgs{ + Msgs: []sdk.Msg{msgDelegate}, + Gas: &gas, + GasPrice: &gasPrice, + }) + + if res.IsErr() { + return fmt.Errorf("tx result with code %d. Logs: %s", res.Code, res.Log) + } + + return err +} + +// CreateValidator executes the transaction to create a validator +// with the parameters specified +func (tf *stakingTxFactory) CreateValidator(operatorPriv cryptotypes.PrivKey, pubKey cryptotypes.PubKey, selfDelegation sdk.Coin, description stakingtypes.Description, commission stakingtypes.CommissionRates, minSelfDelegation math.Int) error { + operatorAccAddr := sdk.ValAddress(operatorPriv.PubKey().Address()) + + msgCreateValidator, err := stakingtypes.NewMsgCreateValidator( + operatorAccAddr.String(), + pubKey, + selfDelegation, + description, + commission, + minSelfDelegation, + ) + if err != nil { + return err + } + + resp, err := tf.ExecuteCosmosTx(operatorPriv, CosmosTxArgs{ + Msgs: []sdk.Msg{msgCreateValidator}, + }) + + if resp.Code != 0 { + err = fmt.Errorf("received error code %d on CreateValidator transaction. Logs: %s", resp.Code, resp.Log) + } + + return err +} diff --git a/testutil/integration/common/factory/types.go b/testutil/integration/common/factory/types.go new file mode 100644 index 0000000..52b6861 --- /dev/null +++ b/testutil/integration/common/factory/types.go @@ -0,0 +1,24 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package factory + +import ( + sdkmath "cosmossdk.io/math" + sdktypes "github.com/cosmos/cosmos-sdk/types" +) + +// CosmosTxArgs contains the params to create a cosmos tx +type CosmosTxArgs struct { + // ChainID is the chain's id in cosmos format, e.g. 'evmos_9000-1' + ChainID string + // Gas to be used on the tx + Gas *uint64 + // GasPrice to use on tx + GasPrice *sdkmath.Int + // Fees is the fee to be used on the tx (amount and denom) + Fees sdktypes.Coins + // FeeGranter is the account address of the fee granter + FeeGranter sdktypes.AccAddress + // Msgs slice of messages to include on the tx + Msgs []sdktypes.Msg +} diff --git a/testutil/integration/common/grpc/account.go b/testutil/integration/common/grpc/account.go new file mode 100644 index 0000000..b53cce0 --- /dev/null +++ b/testutil/integration/common/grpc/account.go @@ -0,0 +1,28 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package grpc + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// GetAccount returns the account for the given address. +func (gqh *IntegrationHandler) GetAccount(address string) (sdk.AccountI, error) { + authClient := gqh.network.GetAuthClient() + res, err := authClient.Account(context.Background(), &authtypes.QueryAccountRequest{ + Address: address, + }) + if err != nil { + return nil, err + } + + encodingCgf := gqh.network.GetEncodingConfig() + var acc sdk.AccountI + if err = encodingCgf.InterfaceRegistry.UnpackAny(res.Account, &acc); err != nil { + return nil, err + } + return acc, nil +} diff --git a/testutil/integration/common/grpc/authz.go b/testutil/integration/common/grpc/authz.go new file mode 100644 index 0000000..bd60044 --- /dev/null +++ b/testutil/integration/common/grpc/authz.go @@ -0,0 +1,117 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package grpc + +import ( + "context" + + "github.com/cosmos/cosmos-sdk/x/authz" +) + +// GetGrants returns the grants for the given grantee and granter combination. +// +// NOTE: To extract the concrete authorizations, use the GetAuthorizations method. +func (gqh *IntegrationHandler) GetGrants(grantee, granter string) ([]*authz.Grant, error) { + authzClient := gqh.network.GetAuthzClient() + res, err := authzClient.Grants(context.Background(), &authz.QueryGrantsRequest{ + Grantee: grantee, + Granter: granter, + }) + if err != nil { + return nil, err + } + + return res.Grants, nil +} + +// GetGrantsByGrantee returns the grants for the given grantee. +// +// NOTE: To extract the concrete authorizations, use the GetAuthorizationsByGrantee method. +func (gqh *IntegrationHandler) GetGrantsByGrantee(grantee string) ([]*authz.GrantAuthorization, error) { + authzClient := gqh.network.GetAuthzClient() + res, err := authzClient.GranteeGrants(context.Background(), &authz.QueryGranteeGrantsRequest{ + Grantee: grantee, + }) + if err != nil { + return nil, err + } + + return res.Grants, nil +} + +// GetGrantsByGranter returns the grants for the given granter. +// +// NOTE: To extract the concrete authorizations, use the GetAuthorizationsByGranter method. +func (gqh *IntegrationHandler) GetGrantsByGranter(granter string) ([]*authz.GrantAuthorization, error) { + authzClient := gqh.network.GetAuthzClient() + res, err := authzClient.GranterGrants(context.Background(), &authz.QueryGranterGrantsRequest{ + Granter: granter, + }) + if err != nil { + return nil, err + } + + return res.Grants, nil +} + +// GetAuthorizations returns the concrete authorizations for the given grantee and granter combination. +func (gqh *IntegrationHandler) GetAuthorizations(grantee, granter string) ([]authz.Authorization, error) { + encodingCfg := gqh.network.GetEncodingConfig() + + grants, err := gqh.GetGrants(grantee, granter) + if err != nil { + return nil, err + } + + auths := make([]authz.Authorization, 0, len(grants)) + for _, grant := range grants { + var auth authz.Authorization + err := encodingCfg.InterfaceRegistry.UnpackAny(grant.Authorization, &auth) + if err != nil { + return nil, err + } + + auths = append(auths, auth) + } + + return auths, nil +} + +// GetAuthorizationsByGrantee returns the concrete authorizations for the given grantee. +func (gqh *IntegrationHandler) GetAuthorizationsByGrantee(grantee string) ([]authz.Authorization, error) { + grants, err := gqh.GetGrantsByGrantee(grantee) + if err != nil { + return nil, err + } + + return gqh.unpackGrantAuthzs(grants) +} + +// GetAuthorizationsByGranter returns the concrete authorizations for the given granter. +func (gqh *IntegrationHandler) GetAuthorizationsByGranter(granter string) ([]authz.Authorization, error) { + grants, err := gqh.GetGrantsByGranter(granter) + if err != nil { + return nil, err + } + + return gqh.unpackGrantAuthzs(grants) +} + +// unpackGrantAuthzs unpacks the given grant authorization. +func (gqh *IntegrationHandler) unpackGrantAuthzs(grantAuthzs []*authz.GrantAuthorization) ([]authz.Authorization, error) { + encodingCfg := gqh.network.GetEncodingConfig() + + auths := make([]authz.Authorization, 0, len(grantAuthzs)) + for _, grantAuthz := range grantAuthzs { + var auth authz.Authorization + err := encodingCfg.InterfaceRegistry.UnpackAny(grantAuthz.Authorization, &auth) + if err != nil { + return nil, err + } + + auths = append(auths, auth) + } + + return auths, nil +} diff --git a/testutil/integration/common/grpc/bank.go b/testutil/integration/common/grpc/bank.go new file mode 100644 index 0000000..2d6675e --- /dev/null +++ b/testutil/integration/common/grpc/bank.go @@ -0,0 +1,40 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package grpc + +import ( + "context" + + sdktypes "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +// GetBalance returns the balance for the given address and denom. +func (gqh *IntegrationHandler) GetBalance(address sdktypes.AccAddress, denom string) (*banktypes.QueryBalanceResponse, error) { + bankClient := gqh.network.GetBankClient() + return bankClient.Balance(context.Background(), &banktypes.QueryBalanceRequest{ + Address: address.String(), + Denom: denom, + }) +} + +// GetAllBalances returns all the balances for the given address. +func (gqh *IntegrationHandler) GetAllBalances(address sdktypes.AccAddress) (*banktypes.QueryAllBalancesResponse, error) { + bankClient := gqh.network.GetBankClient() + return bankClient.AllBalances(context.Background(), &banktypes.QueryAllBalancesRequest{ + Address: address.String(), + }) +} + +// GetTotalSupply returns all the balances for the given address. +func (gqh *IntegrationHandler) GetTotalSupply() (*banktypes.QueryTotalSupplyResponse, error) { + bankClient := gqh.network.GetBankClient() + return bankClient.TotalSupply(context.Background(), &banktypes.QueryTotalSupplyRequest{}) +} + +// GetSpendableBalance returns the spendable balance for the given denomination. +func (gqh *IntegrationHandler) GetSpendableBalance(address sdktypes.AccAddress, denom string) (*banktypes.QuerySpendableBalanceByDenomResponse, error) { + bankClient := gqh.network.GetBankClient() + return bankClient.SpendableBalanceByDenom(context.Background(), &banktypes.QuerySpendableBalanceByDenomRequest{Address: address.String(), Denom: denom}) +} diff --git a/testutil/integration/common/grpc/distribution.go b/testutil/integration/common/grpc/distribution.go new file mode 100644 index 0000000..2e89b41 --- /dev/null +++ b/testutil/integration/common/grpc/distribution.go @@ -0,0 +1,56 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package grpc + +import ( + "context" + + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" +) + +// GetDelegationTotalRewards returns the total delegation rewards for the given delegator. +func (gqh *IntegrationHandler) GetDelegationTotalRewards(delegatorAddress string) (*distrtypes.QueryDelegationTotalRewardsResponse, error) { + distrClient := gqh.network.GetDistrClient() + return distrClient.DelegationTotalRewards(context.Background(), &distrtypes.QueryDelegationTotalRewardsRequest{ + DelegatorAddress: delegatorAddress, + }) +} + +// GetDelegationRewards returns the delegation rewards for the given delegator and validator. +func (gqh *IntegrationHandler) GetDelegationRewards(delegatorAddress string, validatorAddress string) (*distrtypes.QueryDelegationRewardsResponse, error) { + distrClient := gqh.network.GetDistrClient() + return distrClient.DelegationRewards(context.Background(), &distrtypes.QueryDelegationRewardsRequest{ + DelegatorAddress: delegatorAddress, + ValidatorAddress: validatorAddress, + }) +} + +// GetDelegatorWithdrawAddr returns the withdraw address the given delegator. +func (gqh *IntegrationHandler) GetDelegatorWithdrawAddr(delegatorAddress string) (*distrtypes.QueryDelegatorWithdrawAddressResponse, error) { + distrClient := gqh.network.GetDistrClient() + return distrClient.DelegatorWithdrawAddress(context.Background(), &distrtypes.QueryDelegatorWithdrawAddressRequest{ + DelegatorAddress: delegatorAddress, + }) +} + +// GetValidatorCommission returns the commission for the given validator. +func (gqh *IntegrationHandler) GetValidatorCommission(validatorAddress string) (*distrtypes.QueryValidatorCommissionResponse, error) { + distrClient := gqh.network.GetDistrClient() + return distrClient.ValidatorCommission(context.Background(), &distrtypes.QueryValidatorCommissionRequest{ + ValidatorAddress: validatorAddress, + }) +} + +// GetValidatorOutstandingRewards returns the delegation rewards for the given delegator and validator. +func (gqh *IntegrationHandler) GetValidatorOutstandingRewards(validatorAddress string) (*distrtypes.QueryValidatorOutstandingRewardsResponse, error) { + distrClient := gqh.network.GetDistrClient() + return distrClient.ValidatorOutstandingRewards(context.Background(), &distrtypes.QueryValidatorOutstandingRewardsRequest{ + ValidatorAddress: validatorAddress, + }) +} + +// GetCommunityPool queries the community pool coins. +func (gqh *IntegrationHandler) GetCommunityPool() (*distrtypes.QueryCommunityPoolResponse, error) { + distrClient := gqh.network.GetDistrClient() + return distrClient.CommunityPool(context.Background(), &distrtypes.QueryCommunityPoolRequest{}) +} diff --git a/testutil/integration/common/grpc/feemarket.go b/testutil/integration/common/grpc/feemarket.go new file mode 100644 index 0000000..7fa9ee3 --- /dev/null +++ b/testutil/integration/common/grpc/feemarket.go @@ -0,0 +1,13 @@ +package grpc + +import ( + "context" + + feemarkettypes "github.com/evmos/evmos/v20/x/feemarket/types" +) + +// GetBaseFee returns the base fee from the feemarket module. +func (gqh *IntegrationHandler) GetBaseFee() (*feemarkettypes.QueryBaseFeeResponse, error) { + feeMarketClient := gqh.network.GetFeeMarketClient() + return feeMarketClient.BaseFee(context.Background(), &feemarkettypes.QueryBaseFeeRequest{}) +} diff --git a/testutil/integration/common/grpc/gov.go b/testutil/integration/common/grpc/gov.go new file mode 100644 index 0000000..e916c7d --- /dev/null +++ b/testutil/integration/common/grpc/gov.go @@ -0,0 +1,28 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package grpc + +import ( + "fmt" + "slices" + + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// GetGovParams returns the gov params from the gov module. +func (gqh *IntegrationHandler) GetGovParams(paramsType string) (*govtypes.QueryParamsResponse, error) { + possibleTypes := []string{"deposit", "tallying", "voting"} + if !slices.Contains(possibleTypes, paramsType) { + return nil, fmt.Errorf("invalid params type: %s\npossible types: %s", paramsType, possibleTypes) + } + + govClient := gqh.network.GetGovClient() + return govClient.Params(gqh.network.GetContext(), &govtypes.QueryParamsRequest{ParamsType: paramsType}) +} + +// GetProposal returns the proposal from the gov module. +func (gqh *IntegrationHandler) GetProposal(proposalID uint64) (*govtypes.QueryProposalResponse, error) { + govClient := gqh.network.GetGovClient() + return govClient.Proposal(gqh.network.GetContext(), &govtypes.QueryProposalRequest{ProposalId: proposalID}) +} diff --git a/testutil/integration/common/grpc/grpc.go b/testutil/integration/common/grpc/grpc.go new file mode 100644 index 0000000..84423ec --- /dev/null +++ b/testutil/integration/common/grpc/grpc.go @@ -0,0 +1,71 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package grpc + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + feemarkettypes "github.com/evmos/evmos/v20/x/feemarket/types" + network "github.com/xrplevm/node/v6/testutil/integration/common/network" +) + +// Handler is an interface that defines the common methods that are used to query +// the network's modules via gRPC. +type Handler interface { + // Account methods + GetAccount(address string) (sdk.AccountI, error) + + // Authz methods + GetAuthorizations(grantee, granter string) ([]authz.Authorization, error) + GetAuthorizationsByGrantee(grantee string) ([]authz.Authorization, error) + GetAuthorizationsByGranter(granter string) ([]authz.Authorization, error) + GetGrants(grantee, granter string) ([]*authz.Grant, error) + GetGrantsByGrantee(grantee string) ([]*authz.GrantAuthorization, error) + GetGrantsByGranter(granter string) ([]*authz.GrantAuthorization, error) + + // Bank methods + GetBalance(address sdk.AccAddress, denom string) (*banktypes.QueryBalanceResponse, error) + GetSpendableBalance(address sdk.AccAddress, denom string) (*banktypes.QuerySpendableBalanceByDenomResponse, error) + GetAllBalances(address sdk.AccAddress) (*banktypes.QueryAllBalancesResponse, error) + GetTotalSupply() (*banktypes.QueryTotalSupplyResponse, error) + + // Staking methods + GetDelegation(delegatorAddress string, validatorAddress string) (*stakingtypes.QueryDelegationResponse, error) + GetDelegatorDelegations(delegatorAddress string) (*stakingtypes.QueryDelegatorDelegationsResponse, error) + GetValidatorDelegations(validatorAddress string) (*stakingtypes.QueryValidatorDelegationsResponse, error) + GetRedelegations(delegatorAddress, srcValidator, dstValidator string) (*stakingtypes.QueryRedelegationsResponse, error) + GetValidatorUnbondingDelegations(validatorAddress string) (*stakingtypes.QueryValidatorUnbondingDelegationsResponse, error) + GetDelegatorUnbondingDelegations(delegatorAddress string) (*stakingtypes.QueryDelegatorUnbondingDelegationsResponse, error) + + // Distribution methods + GetDelegationTotalRewards(delegatorAddress string) (*distrtypes.QueryDelegationTotalRewardsResponse, error) + GetDelegationRewards(delegatorAddress string, validatorAddress string) (*distrtypes.QueryDelegationRewardsResponse, error) + GetDelegatorWithdrawAddr(delegatorAddress string) (*distrtypes.QueryDelegatorWithdrawAddressResponse, error) + GetValidatorCommission(validatorAddress string) (*distrtypes.QueryValidatorCommissionResponse, error) + GetValidatorOutstandingRewards(validatorAddress string) (*distrtypes.QueryValidatorOutstandingRewardsResponse, error) + GetCommunityPool() (*distrtypes.QueryCommunityPoolResponse, error) + GetBondedValidators() (*stakingtypes.QueryValidatorsResponse, error) + + // FeeMarket methods + GetBaseFee() (*feemarkettypes.QueryBaseFeeResponse, error) +} + +var _ Handler = (*IntegrationHandler)(nil) + +// IntegrationHandler is a helper struct to query the network's modules +// via gRPC. This is to simulate the behavior of a real user and avoid querying +// the modules directly. +type IntegrationHandler struct { + network network.Network +} + +// NewIntegrationHandler creates a new IntegrationHandler instance. +func NewIntegrationHandler(network network.Network) *IntegrationHandler { + return &IntegrationHandler{ + network: network, + } +} diff --git a/testutil/integration/common/grpc/staking.go b/testutil/integration/common/grpc/staking.go new file mode 100644 index 0000000..71cec10 --- /dev/null +++ b/testutil/integration/common/grpc/staking.go @@ -0,0 +1,68 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package grpc + +import ( + "context" + + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// GetDelegation returns the delegation for the given delegator and validator addresses. +func (gqh *IntegrationHandler) GetDelegation(delegatorAddress string, validatorAddress string) (*stakingtypes.QueryDelegationResponse, error) { + stakingClient := gqh.network.GetStakingClient() + return stakingClient.Delegation(context.Background(), &stakingtypes.QueryDelegationRequest{ + DelegatorAddr: delegatorAddress, + ValidatorAddr: validatorAddress, + }) +} + +// GetValidatorDelegations returns the delegations to a given validator. +func (gqh *IntegrationHandler) GetValidatorDelegations(validatorAddress string) (*stakingtypes.QueryValidatorDelegationsResponse, error) { + stakingClient := gqh.network.GetStakingClient() + return stakingClient.ValidatorDelegations(context.Background(), &stakingtypes.QueryValidatorDelegationsRequest{ + ValidatorAddr: validatorAddress, + }) +} + +// GetDelegatorDelegations returns the delegations to a given delegator. +func (gqh *IntegrationHandler) GetDelegatorDelegations(delegatorAddress string) (*stakingtypes.QueryDelegatorDelegationsResponse, error) { + stakingClient := gqh.network.GetStakingClient() + return stakingClient.DelegatorDelegations(context.Background(), &stakingtypes.QueryDelegatorDelegationsRequest{ + DelegatorAddr: delegatorAddress, + }) +} + +// GetRedelegations returns the redelegations to a given delegator and validators. +func (gqh *IntegrationHandler) GetRedelegations(delegatorAddress, srcValidator, dstValidator string) (*stakingtypes.QueryRedelegationsResponse, error) { + stakingClient := gqh.network.GetStakingClient() + return stakingClient.Redelegations(context.Background(), &stakingtypes.QueryRedelegationsRequest{ + DelegatorAddr: delegatorAddress, + SrcValidatorAddr: srcValidator, + DstValidatorAddr: dstValidator, + }) +} + +// GetValidatorUnbondingDelegations returns the unbonding delegations to a given validator. +func (gqh *IntegrationHandler) GetValidatorUnbondingDelegations(validatorAddress string) (*stakingtypes.QueryValidatorUnbondingDelegationsResponse, error) { + stakingClient := gqh.network.GetStakingClient() + return stakingClient.ValidatorUnbondingDelegations(context.Background(), &stakingtypes.QueryValidatorUnbondingDelegationsRequest{ + ValidatorAddr: validatorAddress, + }) +} + +// GetDelegatorUnbondingDelegations returns all the unbonding delegations for given delegator. +func (gqh *IntegrationHandler) GetDelegatorUnbondingDelegations(delegatorAddress string) (*stakingtypes.QueryDelegatorUnbondingDelegationsResponse, error) { + stakingClient := gqh.network.GetStakingClient() + return stakingClient.DelegatorUnbondingDelegations(context.Background(), &stakingtypes.QueryDelegatorUnbondingDelegationsRequest{ + DelegatorAddr: delegatorAddress, + }) +} + +// GetValidators returns the list of all bonded validators. +func (gqh *IntegrationHandler) GetBondedValidators() (*stakingtypes.QueryValidatorsResponse, error) { + stakingClient := gqh.network.GetStakingClient() + return stakingClient.Validators(context.Background(), &stakingtypes.QueryValidatorsRequest{ + Status: stakingtypes.BondStatusBonded, + }) +} diff --git a/testutil/integration/common/keyring/keyring.go b/testutil/integration/common/keyring/keyring.go new file mode 100644 index 0000000..013e499 --- /dev/null +++ b/testutil/integration/common/keyring/keyring.go @@ -0,0 +1,119 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package keyring + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" + utiltx "github.com/evmos/evmos/v20/testutil/tx" +) + +type Key struct { + Addr common.Address + AccAddr sdktypes.AccAddress + Priv cryptotypes.PrivKey +} + +func NewKey() Key { + addr, privKey := utiltx.NewAddrKey() + return Key{ + Addr: addr, + AccAddr: sdktypes.AccAddress(addr.Bytes()), + Priv: privKey, + } +} + +type Keyring interface { + // GetPrivKey returns the private key of the account at the given keyring index. + GetPrivKey(index int) cryptotypes.PrivKey + // GetAddr returns the address of the account at the given keyring index. + GetAddr(index int) common.Address + // GetAccAddr returns the SDK address of the account at the given keyring index. + GetAccAddr(index int) sdktypes.AccAddress + // GetAllAccAddrs returns all the SDK addresses of the accounts in the keyring. + GetAllAccAddrs() []sdktypes.AccAddress + // GetKey returns the key at the given keyring index + GetKey(index int) Key + // GetKeys returns all the keys + GetKeys() []Key + + // AddKey adds a new account to the keyring + AddKey() int + + // Sign signs message with the specified account. + Sign(index int, msg []byte) ([]byte, error) +} + +// IntegrationKeyring is a keyring designed for integration tests. +type IntegrationKeyring struct { + keys []Key +} + +var _ Keyring = (*IntegrationKeyring)(nil) + +// New returns a new keyring with nAccs accounts. +func New(nAccs int) Keyring { + accs := make([]Key, 0, nAccs) + for i := 0; i < nAccs; i++ { + acc := NewKey() + accs = append(accs, acc) + } + return &IntegrationKeyring{ + keys: accs, + } +} + +// GetPrivKey returns the private key of the specified account. +func (kr *IntegrationKeyring) GetPrivKey(index int) cryptotypes.PrivKey { + return kr.keys[index].Priv +} + +// GetAddr returns the address of the specified account. +func (kr *IntegrationKeyring) GetAddr(index int) common.Address { + return kr.keys[index].Addr +} + +// GetAccAddr returns the sdk address of the specified account. +func (kr *IntegrationKeyring) GetAccAddr(index int) sdktypes.AccAddress { + return kr.keys[index].AccAddr +} + +// GetAllAccAddrs returns all the sdk addresses of the accounts in the keyring. +func (kr *IntegrationKeyring) GetAllAccAddrs() []sdktypes.AccAddress { + accs := make([]sdktypes.AccAddress, 0, len(kr.keys)) + for _, key := range kr.keys { + accs = append(accs, key.AccAddr) + } + return accs +} + +// GetKey returns the key specified by index +func (kr *IntegrationKeyring) GetKey(index int) Key { + return kr.keys[index] +} + +// GetKey returns the key specified by index +func (kr *IntegrationKeyring) GetKeys() []Key { + return kr.keys +} + +// AddKey adds a new account to the keyring. It returns the index for the key +func (kr *IntegrationKeyring) AddKey() int { + acc := NewKey() + index := len(kr.keys) + kr.keys = append(kr.keys, acc) + return index +} + +// Sign signs message with the specified key. +func (kr *IntegrationKeyring) Sign(index int, msg []byte) ([]byte, error) { + privKey := kr.GetPrivKey(index) + if privKey == nil { + return nil, fmt.Errorf("no private key for account %d", index) + } + return privKey.Sign(msg) +} diff --git a/testutil/integration/common/network/network.go b/testutil/integration/common/network/network.go new file mode 100644 index 0000000..bb98e18 --- /dev/null +++ b/testutil/integration/common/network/network.go @@ -0,0 +1,58 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package network + +import ( + "testing" + "time" + + sdkmath "cosmossdk.io/math" + abcitypes "github.com/cometbft/cometbft/abci/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" + sdktestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + authz "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" + feemarkettypes "github.com/evmos/evmos/v20/x/feemarket/types" +) + +// Network is the interface that wraps the common methods to interact with integration test network. +// +// It was designed to avoid users to access module's keepers directly and force integration tests +// to be closer to the real user's behavior. +type Network interface { + GetContext() sdktypes.Context + GetChainID() string + GetDenom() string + GetOtherDenoms() []string + GetValidators() []stakingtypes.Validator + GetMinDepositAmt() sdkmath.Int + NextBlock() error + NextBlockAfter(duration time.Duration) error + NextBlockWithTxs(txBytes ...[]byte) (*abcitypes.ResponseFinalizeBlock, error) + + // Clients + GetAuthClient() authtypes.QueryClient + GetAuthzClient() authz.QueryClient + GetBankClient() banktypes.QueryClient + GetStakingClient() stakingtypes.QueryClient + GetDistrClient() distrtypes.QueryClient + GetFeeMarketClient() feemarkettypes.QueryClient + GetGovClient() govtypes.QueryClient + + BroadcastTxSync(txBytes []byte) (abcitypes.ExecTxResult, error) + Simulate(txBytes []byte) (*txtypes.SimulateResponse, error) + CheckTx(txBytes []byte) (*abcitypes.ResponseCheckTx, error) + + // GetIBCChain returns the IBC test chain. + // NOTE: this is only used for testing IBC related functionality. + // The idea is to deprecate this eventually. + GetIBCChain(t *testing.T, coord *ibctesting.Coordinator) *ibctesting.TestChain + GetEncodingConfig() sdktestutil.TestEncodingConfig +} diff --git a/testutil/integration/exrp/common/clients.go b/testutil/integration/exrp/common/clients.go new file mode 100644 index 0000000..f10d2db --- /dev/null +++ b/testutil/integration/exrp/common/clients.go @@ -0,0 +1,123 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package exrpcommon + +import ( + "github.com/cosmos/cosmos-sdk/baseapp" + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module/testutil" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + erc20keeper "github.com/evmos/evmos/v20/x/erc20/keeper" + erc20types "github.com/evmos/evmos/v20/x/erc20/types" + evmkeeper "github.com/evmos/evmos/v20/x/evm/keeper" + evmtypes "github.com/evmos/evmos/v20/x/evm/types" + feemarketkeeper "github.com/evmos/evmos/v20/x/feemarket/keeper" + feemarkettypes "github.com/evmos/evmos/v20/x/feemarket/types" + poakeeper "github.com/xrplevm/node/v6/x/poa/keeper" + poatypes "github.com/xrplevm/node/v6/x/poa/types" +) + +type NetworkKeepers interface { + GetContext() sdktypes.Context + GetEncodingConfig() testutil.TestEncodingConfig + + ERC20Keeper() erc20keeper.Keeper + EvmKeeper() evmkeeper.Keeper + GovKeeper() *govkeeper.Keeper + BankKeeper() bankkeeper.Keeper + StakingKeeper() *stakingkeeper.Keeper + SlashingKeeper() slashingkeeper.Keeper + DistrKeeper() distrkeeper.Keeper + AccountKeeper() authkeeper.AccountKeeper + AuthzKeeper() authzkeeper.Keeper + FeeMarketKeeper() feemarketkeeper.Keeper + PoaKeeper() poakeeper.Keeper +} + +func getQueryHelper(ctx sdktypes.Context, encCfg testutil.TestEncodingConfig) *baseapp.QueryServiceTestHelper { + interfaceRegistry := encCfg.InterfaceRegistry + // This is needed so that state changes are not committed in precompiles + // simulations. + cacheCtx, _ := ctx.CacheContext() + return baseapp.NewQueryServerTestHelper(cacheCtx, interfaceRegistry) +} + +func GetERC20Client(n NetworkKeepers) erc20types.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + erc20types.RegisterQueryServer(queryHelper, n.ERC20Keeper()) + return erc20types.NewQueryClient(queryHelper) +} + +func GetEvmClient(n NetworkKeepers) evmtypes.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + evmtypes.RegisterQueryServer(queryHelper, n.EvmKeeper()) + return evmtypes.NewQueryClient(queryHelper) +} + +func GetGovClient(n NetworkKeepers) govtypes.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + govtypes.RegisterQueryServer(queryHelper, govkeeper.NewQueryServer(n.GovKeeper())) + return govtypes.NewQueryClient(queryHelper) +} + +func GetBankClient(n NetworkKeepers) banktypes.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + banktypes.RegisterQueryServer(queryHelper, n.BankKeeper()) + return banktypes.NewQueryClient(queryHelper) +} + +func GetFeeMarketClient(n NetworkKeepers) feemarkettypes.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + feemarkettypes.RegisterQueryServer(queryHelper, n.FeeMarketKeeper()) + return feemarkettypes.NewQueryClient(queryHelper) +} + +func GetAuthClient(n NetworkKeepers) authtypes.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + authtypes.RegisterQueryServer(queryHelper, authkeeper.NewQueryServer(n.AccountKeeper())) + return authtypes.NewQueryClient(queryHelper) +} + +func GetAuthzClient(n NetworkKeepers) authz.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + authz.RegisterQueryServer(queryHelper, n.AuthzKeeper()) + return authz.NewQueryClient(queryHelper) +} + +func GetStakingClient(n NetworkKeepers) stakingtypes.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + stakingtypes.RegisterQueryServer(queryHelper, stakingkeeper.Querier{Keeper: n.StakingKeeper()}) + return stakingtypes.NewQueryClient(queryHelper) +} + +func GetSlashingClient(n NetworkKeepers) slashingtypes.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + slashingtypes.RegisterQueryServer(queryHelper, slashingkeeper.Querier{Keeper: n.SlashingKeeper()}) + return slashingtypes.NewQueryClient(queryHelper) +} + +func GetDistrClient(n NetworkKeepers) distrtypes.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + distrtypes.RegisterQueryServer(queryHelper, distrkeeper.Querier{Keeper: n.DistrKeeper()}) + return distrtypes.NewQueryClient(queryHelper) +} + +func GetPoaClient(n NetworkKeepers) poatypes.QueryClient { + queryHelper := getQueryHelper(n.GetContext(), n.GetEncodingConfig()) + poatypes.RegisterQueryServer(queryHelper, poakeeper.Querier{Keeper: n.PoaKeeper()}) + return poatypes.NewQueryClient(queryHelper) +} diff --git a/testutil/integration/exrp/common/config.go b/testutil/integration/exrp/common/config.go new file mode 100644 index 0000000..3e3527c --- /dev/null +++ b/testutil/integration/exrp/common/config.go @@ -0,0 +1,151 @@ +package exrpcommon + +import ( + "math/big" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/baseapp" + sdktypes "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + evmostypes "github.com/evmos/evmos/v20/types" + "github.com/xrplevm/node/v6/app" +) + +const ( + bip44CoinType = 60 + ChainID = "exrp_1440002-1" +) + +// Config defines the configuration for a chain. +// It allows for customization of the network to adjust to +// testing needs. +type Config struct { + ChainID string + EIP155ChainID *big.Int + AmountOfValidators int + PreFundedAccounts []sdktypes.AccAddress + Balances []banktypes.Balance + BondDenom string + MaxValidators uint32 + Denom string + CustomGenesisState CustomGenesisState + GenesisBytes []byte + OtherCoinDenom []string + OperatorsAddrs []sdktypes.AccAddress + CustomBaseAppOpts []func(*baseapp.BaseApp) + MinDepositAmt sdkmath.Int + Quorum string +} + +type CustomGenesisState map[string]interface{} + +// DefaultConfig returns the default configuration for a chain. +func DefaultConfig() Config { + return Config{ + ChainID: ChainID, + EIP155ChainID: big.NewInt(1440002), + Balances: nil, + Denom: app.BaseDenom, + } +} + +// ConfigOption defines a function that can modify the NetworkConfig. +// The purpose of this is to force to be declarative when the default configuration +// requires to be changed. +type ConfigOption func(*Config) + +// WithChainID sets a custom chainID for the network. It panics if the chainID is invalid. +func WithChainID(chainID string) ConfigOption { + chainIDNum, err := evmostypes.ParseChainID(chainID) + if err != nil { + panic(err) + } + return func(cfg *Config) { + cfg.ChainID = chainID + cfg.EIP155ChainID = chainIDNum + } +} + +// WithAmountOfValidators sets the amount of validators for the network. +func WithAmountOfValidators(amount int) ConfigOption { + return func(cfg *Config) { + cfg.AmountOfValidators = amount + } +} + +// WithPreFundedAccounts sets the pre-funded accounts for the network. +func WithPreFundedAccounts(accounts ...sdktypes.AccAddress) ConfigOption { + return func(cfg *Config) { + cfg.PreFundedAccounts = accounts + } +} + +// WithBalances sets the specific balances for the pre-funded accounts, that +// are being set up for the network. +func WithBalances(balances ...banktypes.Balance) ConfigOption { + return func(cfg *Config) { + cfg.Balances = append(cfg.Balances, balances...) + } +} + +// WithBondDenom sets the bond denom for the network. +func WithBondDenom(denom string) ConfigOption { + return func(cfg *Config) { + cfg.BondDenom = denom + } +} + +// WithMaxValidators sets the max validators for the network. +func WithMaxValidators(maxValidators uint32) ConfigOption { + return func(cfg *Config) { + cfg.MaxValidators = maxValidators + } +} + +// WithDenom sets the denom for the network. +func WithDenom(denom string) ConfigOption { + return func(cfg *Config) { + cfg.Denom = denom + } +} + +// WithCustomGenesis sets the custom genesis of the network for specific modules. +func WithCustomGenesis(customGenesis CustomGenesisState) ConfigOption { + return func(cfg *Config) { + cfg.CustomGenesisState = customGenesis + } +} + +// WithOtherDenoms sets other possible coin denominations for the network. +func WithOtherDenoms(otherDenoms []string) ConfigOption { + return func(cfg *Config) { + cfg.OtherCoinDenom = otherDenoms + } +} + +// WithValidatorOperators overwrites the used operator address for the network instantiation. +func WithValidatorOperators(keys []sdktypes.AccAddress) ConfigOption { + return func(cfg *Config) { + cfg.OperatorsAddrs = keys + } +} + +// WithCustomBaseAppOpts sets custom base app options for the network. +func WithCustomBaseAppOpts(opts ...func(*baseapp.BaseApp)) ConfigOption { + return func(cfg *Config) { + cfg.CustomBaseAppOpts = opts + } +} + +// WithMinDepositAmt sets the min deposit amount for the network. +func WithMinDepositAmt(minDepositAmt sdkmath.Int) ConfigOption { + return func(cfg *Config) { + cfg.MinDepositAmt = minDepositAmt + } +} + +func WithQuorum(quorum string) ConfigOption { + return func(cfg *Config) { + cfg.Quorum = quorum + } +} diff --git a/testutil/integration/exrp/common/consensus.go b/testutil/integration/exrp/common/consensus.go new file mode 100644 index 0000000..1965e2d --- /dev/null +++ b/testutil/integration/exrp/common/consensus.go @@ -0,0 +1,27 @@ +package exrpcommon + +import ( + "time" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" +) + +// DefaultConsensusParams defines the default Tendermint consensus params used in +// Evmos testing. +var DefaultConsensusParams = &cmtproto.ConsensusParams{ + Block: &cmtproto.BlockParams{ + MaxBytes: 200000, + MaxGas: -1, // no limit + }, + Evidence: &cmtproto.EvidenceParams{ + MaxAgeNumBlocks: 302400, + MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration + MaxBytes: 10000, + }, + Validator: &cmtproto.ValidatorParams{ + PubKeyTypes: []string{ + cmttypes.ABCIPubKeyTypeEd25519, + }, + }, +} diff --git a/testutil/integration/exrp/common/network.go b/testutil/integration/exrp/common/network.go new file mode 100644 index 0000000..75ad0a3 --- /dev/null +++ b/testutil/integration/exrp/common/network.go @@ -0,0 +1,43 @@ +package exrpcommon + +import ( + "testing" + "time" + + sdktypes "github.com/cosmos/cosmos-sdk/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" + + abcitypes "github.com/cometbft/cometbft/abci/types" + sdktestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +type Network interface { + // Keepers + NetworkKeepers + + // Clients + BroadcastTxSync(txBytes []byte) (abcitypes.ExecTxResult, error) + Simulate(txBytes []byte) (*txtypes.SimulateResponse, error) + CheckTx(txBytes []byte) (*abcitypes.ResponseCheckTx, error) + + // GetIBCChain returns the IBC test chain. + // NOTE: this is only used for testing IBC related functionality. + // The idea is to deprecate this eventually. + GetIBCChain(t *testing.T, coord *ibctesting.Coordinator) *ibctesting.TestChain + GetEncodingConfig() sdktestutil.TestEncodingConfig + + // Getters + GetContext() sdktypes.Context + GetChainID() string + GetBondDenom() string + GetDenom() string + GetOtherDenoms() []string + GetValidators() []stakingtypes.Validator + + // ABCI + NextBlock() error + NextBlockAfter(duration time.Duration) error + NextBlockWithTxs(txBytes ...[]byte) (*abcitypes.ResponseFinalizeBlock, error) +} diff --git a/testutil/integration/exrp/common/setup.go b/testutil/integration/exrp/common/setup.go new file mode 100644 index 0000000..fbb0d65 --- /dev/null +++ b/testutil/integration/exrp/common/setup.go @@ -0,0 +1,123 @@ +package exrpcommon + +import ( + "fmt" + "os" + + "cosmossdk.io/log" + "github.com/cosmos/cosmos-sdk/baseapp" + sdktypes "github.com/cosmos/cosmos-sdk/types" + + dbm "github.com/cosmos/cosmos-db" + simutils "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/gogoproto/proto" + "github.com/xrplevm/node/v6/app" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" + erc20types "github.com/evmos/evmos/v20/x/erc20/types" + evmtypes "github.com/evmos/evmos/v20/x/evm/types" + feemarkettypes "github.com/evmos/evmos/v20/x/feemarket/types" +) + +// GenSetupFn is the type for the module genesis setup functions +type GenSetupFn func(exrpApp *app.App, genesisState app.GenesisState, customGenesis interface{}) (app.GenesisState, error) + +var genesisSetupFunctions = map[string]GenSetupFn{ + evmtypes.ModuleName: GenStateSetter[*evmtypes.GenesisState](evmtypes.ModuleName), + erc20types.ModuleName: GenStateSetter[*erc20types.GenesisState](erc20types.ModuleName), + govtypes.ModuleName: GenStateSetter[*govtypesv1.GenesisState](govtypes.ModuleName), + feemarkettypes.ModuleName: GenStateSetter[*feemarkettypes.GenesisState](feemarkettypes.ModuleName), + distrtypes.ModuleName: GenStateSetter[*distrtypes.GenesisState](distrtypes.ModuleName), + banktypes.ModuleName: GenStateSetter[*banktypes.GenesisState](banktypes.ModuleName), + authtypes.ModuleName: GenStateSetter[*authtypes.GenesisState](authtypes.ModuleName), + capabilitytypes.ModuleName: GenStateSetter[*capabilitytypes.GenesisState](capabilitytypes.ModuleName), + genutiltypes.ModuleName: GenStateSetter[*genutiltypes.GenesisState](genutiltypes.ModuleName), +} + +// GenStateSetter is a generic function to set module-specific genesis state +func GenStateSetter[T proto.Message](moduleName string) GenSetupFn { + return func(exrpApp *app.App, genesisState app.GenesisState, customGenesis interface{}) (app.GenesisState, error) { + moduleGenesis, ok := customGenesis.(T) + if !ok { + return nil, fmt.Errorf("invalid type %T for %s module genesis state", customGenesis, moduleName) + } + + genesisState[moduleName] = exrpApp.AppCodec().MustMarshalJSON(moduleGenesis) + return genesisState, nil + } +} + +// CustomizeGenesis modifies genesis state if there're any custom genesis state +// for specific modules +func CustomizeGenesis(exrpApp *app.App, customGen CustomGenesisState, genesisState app.GenesisState) (app.GenesisState, error) { + var err error + for mod, modGenState := range customGen { + if fn, found := genesisSetupFunctions[mod]; found { + genesisState, err = fn(exrpApp, genesisState, modGenState) + if err != nil { + return genesisState, err + } + } else { + panic(fmt.Sprintf("module %s not found in genesis setup functions", mod)) + } + } + return genesisState, err +} + +func SetupSdkConfig() { + accountPubKeyPrefix := app.AccountAddressPrefix + "pub" + validatorAddressPrefix := app.AccountAddressPrefix + "valoper" + validatorPubKeyPrefix := app.AccountAddressPrefix + "valoperpub" + consNodeAddressPrefix := app.AccountAddressPrefix + "valcons" + consNodePubKeyPrefix := app.AccountAddressPrefix + "valconspub" + + // Set config + config := sdktypes.GetConfig() + config.SetBech32PrefixForAccount(app.AccountAddressPrefix, accountPubKeyPrefix) + config.SetBech32PrefixForValidator(validatorAddressPrefix, validatorPubKeyPrefix) + config.SetBech32PrefixForConsensusNode(consNodeAddressPrefix, consNodePubKeyPrefix) + config.SetCoinType(bip44CoinType) + config.SetPurpose(sdktypes.Purpose) // Shared + config.Seal() +} + +func MustGetIntegrationTestNodeHome() string { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + return wd + "/../../" +} + +// createExrpApp creates an exrp app +func CreateExrpApp(chainID string, customBaseAppOptions ...func(*baseapp.BaseApp)) *app.App { + testNodeHome := MustGetIntegrationTestNodeHome() + // Create exrp app + db := dbm.NewMemDB() + logger := log.NewNopLogger() + loadLatest := true + skipUpgradeHeights := map[int64]bool{} + homePath := testNodeHome + invCheckPeriod := uint(5) + appOptions := simutils.NewAppOptionsWithFlagHome(homePath) + baseAppOptions := append(customBaseAppOptions, baseapp.SetChainID(chainID)) //nolint:gocritic + + return app.New( + logger, + db, + nil, + loadLatest, + skipUpgradeHeights, + homePath, + invCheckPeriod, + appOptions, + baseAppOptions..., + ) +} diff --git a/testutil/integration/exrp/integration/abci.go b/testutil/integration/exrp/integration/abci.go new file mode 100644 index 0000000..d7328c9 --- /dev/null +++ b/testutil/integration/exrp/integration/abci.go @@ -0,0 +1,129 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package exrpintegration + +import ( + "time" + + storetypes "cosmossdk.io/store/types" + abcitypes "github.com/cometbft/cometbft/abci/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" +) + +// NextBlock is a private helper function that runs the EndBlocker logic, commits the changes, +// updates the header and runs the BeginBlocker +func (n *IntegrationNetwork) NextBlock() error { + return n.NextBlockAfter(time.Second) +} + +// NextBlockAfter is a private helper function that runs the FinalizeBlock logic, updates the context and +// commits the changes to have a block time after the given duration. +func (n *IntegrationNetwork) NextBlockAfter(duration time.Duration) error { + _, err := n.finalizeBlockAndCommit(duration, nil, nil) + return err +} + +// NextBlockWithTxs is a helper function that runs the FinalizeBlock logic +// with the provided tx bytes, updates the context and +// commits the changes to have a block time after the given duration. +func (n *IntegrationNetwork) NextBlockWithTxs(txBytes ...[]byte) (*abcitypes.ResponseFinalizeBlock, error) { + return n.finalizeBlockAndCommit(time.Second, n.valFlags, nil, txBytes...) +} + +// NextNBlocksWithValidatorFlags is a helper function that runs the FinalizeBlock logic +// with the provided validator flags, updates the context and +// commits the changes to have a block time after the given duration. +func (n *IntegrationNetwork) NextNBlocksWithValidatorFlags(blocks int64, validatorFlags []cmtproto.BlockIDFlag) error { + for i := int64(0); i < blocks; i++ { + _, err := n.finalizeBlockAndCommit(time.Second, validatorFlags, nil) + if err != nil { + return err + } + } + return nil +} + +// NextBlockWithMisBehaviors is a helper function that runs the FinalizeBlock logic +// with the provided misbehaviors, updates the context and +// commits the changes to have a block time after the given duration. +func (n *IntegrationNetwork) NextBlockWithMisBehaviors(misbehaviors []abcitypes.Misbehavior) error { + _, err := n.finalizeBlockAndCommit(time.Second, n.valFlags, misbehaviors) + return err +} + +// finalizeBlockAndCommit is a private helper function that runs the FinalizeBlock logic +// with the provided txBytes, updates the context and +// commits the changes to have a block time after the given duration. +func (n *IntegrationNetwork) finalizeBlockAndCommit(duration time.Duration, vFlags []cmtproto.BlockIDFlag, misbehaviors []abcitypes.Misbehavior, txBytes ...[]byte) (*abcitypes.ResponseFinalizeBlock, error) { + header := n.ctx.BlockHeader() + // Update block header and BeginBlock + header.Height++ + header.AppHash = n.app.LastCommitID().Hash + // Calculate new block time after duration + newBlockTime := header.Time.Add(duration) + header.Time = newBlockTime + + var validatorFlags []cmtproto.BlockIDFlag + if len(vFlags) > 0 { + validatorFlags = vFlags + } else { + validatorFlags = n.valFlags + } + + // FinalizeBlock to run endBlock, deliverTx & beginBlock logic + req := BuildFinalizeBlockReq(header, n.valSet.Validators, validatorFlags, misbehaviors, txBytes...) + + res, err := n.app.FinalizeBlock(req) + if err != nil { + return nil, err + } + + newCtx := n.app.BaseApp.NewContextLegacy(false, header) + + // Update context header + newCtx = newCtx.WithMinGasPrices(n.ctx.MinGasPrices()) + newCtx = newCtx.WithKVGasConfig(n.ctx.KVGasConfig()) + newCtx = newCtx.WithTransientKVGasConfig(n.ctx.TransientKVGasConfig()) + newCtx = newCtx.WithConsensusParams(n.ctx.ConsensusParams()) + // This might have to be changed with time if we want to test gas limits + newCtx = newCtx.WithBlockGasMeter(storetypes.NewInfiniteGasMeter()) + newCtx = newCtx.WithVoteInfos(req.DecidedLastCommit.GetVotes()) + n.ctx = newCtx + + // commit changes + _, err = n.app.Commit() + + return res, err +} + +// buildFinalizeBlockReq is a helper function to build +// properly the FinalizeBlock request +func BuildFinalizeBlockReq(header cmtproto.Header, validators []*cmttypes.Validator, validatorFlags []cmtproto.BlockIDFlag, misbehaviors []abcitypes.Misbehavior, txs ...[]byte) *abcitypes.RequestFinalizeBlock { + // add validator's commit info to allocate corresponding tokens to validators + ci := GetCommitInfo(validators, validatorFlags) + return &abcitypes.RequestFinalizeBlock{ + Misbehavior: misbehaviors, + Height: header.Height, + DecidedLastCommit: ci, + Hash: header.AppHash, + NextValidatorsHash: header.ValidatorsHash, + ProposerAddress: header.ProposerAddress, + Time: header.Time, + Txs: txs, + } +} + +func GetCommitInfo(validators []*cmttypes.Validator, validatorFlags []cmtproto.BlockIDFlag) abcitypes.CommitInfo { + voteInfos := make([]abcitypes.VoteInfo, len(validators)) + for i, val := range validators { + voteInfos[i] = abcitypes.VoteInfo{ + Validator: abcitypes.Validator{ + Address: val.Address, + Power: val.VotingPower, + }, + BlockIdFlag: validatorFlags[i], + } + } + return abcitypes.CommitInfo{Votes: voteInfos} +} diff --git a/testutil/integration/exrp/integration/config.go b/testutil/integration/exrp/integration/config.go new file mode 100644 index 0000000..fb2b49b --- /dev/null +++ b/testutil/integration/exrp/integration/config.go @@ -0,0 +1,52 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package exrpintegration + +import ( + "fmt" + + sdktypes "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + testtx "github.com/evmos/evmos/v20/testutil/tx" + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" +) + +// DefaultIntegrationConfig returns the default configuration for a chain. +func DefaultIntegrationConfig() exrpcommon.Config { + account, _ := testtx.NewAccAddressAndKey() + config := exrpcommon.DefaultConfig() + config.AmountOfValidators = 3 + config.PreFundedAccounts = []sdktypes.AccAddress{account} + return config +} + +// getGenAccountsAndBalances takes the network configuration and returns the used +// genesis accounts and balances. +// +// NOTE: If the balances are set, the pre-funded accounts are ignored. +func getGenAccountsAndBalances(cfg exrpcommon.Config, validators []stakingtypes.Validator) (genAccounts []authtypes.GenesisAccount, balances []banktypes.Balance) { + if len(cfg.Balances) > 0 { + balances = cfg.Balances + accounts := getAccAddrsFromBalances(balances) + genAccounts = createGenesisAccounts(accounts) + } else { + genAccounts = createGenesisAccounts(cfg.PreFundedAccounts) + balances = createBalances(cfg.PreFundedAccounts, append(cfg.OtherCoinDenom, cfg.Denom)) + } + + // append validators to genesis accounts and balances + valAccs := make([]sdktypes.AccAddress, len(validators)) + for i, v := range validators { + valAddr, err := sdktypes.ValAddressFromBech32(v.OperatorAddress) + if err != nil { + panic(fmt.Sprintf("failed to derive validator address from %q: %s", v.OperatorAddress, err.Error())) + } + valAccs[i] = sdktypes.AccAddress(valAddr.Bytes()) + } + genAccounts = append(genAccounts, createGenesisAccounts(valAccs)...) + + return +} diff --git a/testutil/integration/exrp/integration/ibc.go b/testutil/integration/exrp/integration/ibc.go new file mode 100644 index 0000000..7d7a316 --- /dev/null +++ b/testutil/integration/exrp/integration/ibc.go @@ -0,0 +1,28 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package exrpintegration + +import ( + "testing" + + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +// GetIBCChain returns a TestChain instance for the given network. +// Note: the sender accounts are not populated. Do not use this accounts to send transactions during tests. +// The keyring should be used instead. +func (n *IntegrationNetwork) GetIBCChain(t *testing.T, coord *ibctesting.Coordinator) *ibctesting.TestChain { + return &ibctesting.TestChain{ + TB: t, + Coordinator: coord, + ChainID: n.GetChainID(), + App: n.app, + CurrentHeader: n.ctx.BlockHeader(), + QueryServer: n.app.GetIBCKeeper(), + TxConfig: n.app.GetTxConfig(), + Codec: n.app.AppCodec(), + Vals: n.valSet, + NextVals: n.valSet, + Signers: n.valSigners, + } +} diff --git a/testutil/integration/exrp/integration/keepers.go b/testutil/integration/exrp/integration/keepers.go new file mode 100644 index 0000000..067b6ff --- /dev/null +++ b/testutil/integration/exrp/integration/keepers.go @@ -0,0 +1,60 @@ +package exrpintegration + +import ( + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + + erc20keeper "github.com/evmos/evmos/v20/x/erc20/keeper" + evmkeeper "github.com/evmos/evmos/v20/x/evm/keeper" + feemarketkeeper "github.com/evmos/evmos/v20/x/feemarket/keeper" + poakeeper "github.com/xrplevm/node/v6/x/poa/keeper" +) + +func (n *IntegrationNetwork) BankKeeper() bankkeeper.Keeper { + return n.app.BankKeeper +} + +func (n *IntegrationNetwork) ERC20Keeper() erc20keeper.Keeper { + return n.app.Erc20Keeper +} + +func (n *IntegrationNetwork) EvmKeeper() evmkeeper.Keeper { + return *n.app.EvmKeeper +} + +func (n *IntegrationNetwork) GovKeeper() *govkeeper.Keeper { + return &n.app.GovKeeper +} + +func (n *IntegrationNetwork) StakingKeeper() *stakingkeeper.Keeper { + return n.app.StakingKeeper.Keeper +} + +func (n *IntegrationNetwork) SlashingKeeper() slashingkeeper.Keeper { + return n.app.SlashingKeeper +} + +func (n *IntegrationNetwork) DistrKeeper() distrkeeper.Keeper { + return n.app.DistrKeeper +} + +func (n *IntegrationNetwork) AccountKeeper() authkeeper.AccountKeeper { + return n.app.AccountKeeper +} + +func (n *IntegrationNetwork) AuthzKeeper() authzkeeper.Keeper { + return n.app.AuthzKeeper +} + +func (n *IntegrationNetwork) FeeMarketKeeper() feemarketkeeper.Keeper { + return n.app.FeeMarketKeeper +} + +func (n *IntegrationNetwork) PoaKeeper() poakeeper.Keeper { + return n.app.PoaKeeper +} diff --git a/testutil/integration/exrp/integration/network.go b/testutil/integration/exrp/integration/network.go new file mode 100644 index 0000000..1ebbe35 --- /dev/null +++ b/testutil/integration/exrp/integration/network.go @@ -0,0 +1,361 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package exrpintegration + +import ( + "fmt" + "math" + "math/big" + "time" + + sdkmath "cosmossdk.io/math" + + gethparams "github.com/ethereum/go-ethereum/params" + "github.com/xrplevm/node/v6/app" + + "github.com/evmos/evmos/v20/types" + + abcitypes "github.com/cometbft/cometbft/abci/types" + cmtjson "github.com/cometbft/cometbft/libs/json" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmversion "github.com/cometbft/cometbft/proto/tendermint/version" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/cometbft/cometbft/version" + sdktypes "github.com/cosmos/cosmos-sdk/types" + sdktestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" +) + +// Network is the interface that wraps the methods to interact with integration test network. +// +// It was designed to avoid users to access module's keepers directly and force integration tests +// to be closer to the real user's behavior. +type Network interface { + exrpcommon.Network + + GetEIP155ChainID() *big.Int + GetValidatorSet() *cmttypes.ValidatorSet +} + +var _ Network = (*IntegrationNetwork)(nil) + +// IntegrationNetwork is the implementation of the Network interface for integration tests. +type IntegrationNetwork struct { + cfg exrpcommon.Config + ctx sdktypes.Context + validators []stakingtypes.Validator + app *app.App + + // This is only needed for IBC chain testing setup + valSet *cmttypes.ValidatorSet + valFlags []cmtproto.BlockIDFlag + valSigners map[string]cmttypes.PrivValidator +} + +// New configures and initializes a new integration Network instance with +// the given configuration options. If no configuration options are provided +// it uses the default configuration. +// +// It panics if an error occurs. +func New(opts ...exrpcommon.ConfigOption) *IntegrationNetwork { + cfg := DefaultIntegrationConfig() + // Modify the default config with the given options + for _, opt := range opts { + opt(&cfg) + } + + ctx := sdktypes.Context{} + network := &IntegrationNetwork{ + cfg: cfg, + ctx: ctx, + validators: []stakingtypes.Validator{}, + } + + err := network.configureAndInitChain() + if err != nil { + panic(err) + } + return network +} + +var ( + // DefaultBondedAmount is the amount of tokens that each validator will have initially bonded + DefaultBondedAmount = sdktypes.TokensFromConsensusPower(1, sdktypes.DefaultPowerReduction) + // PrefundedAccountInitialBalance is the amount of tokens that each prefunded account has at genesis + PrefundedAccountInitialBalance, _ = sdkmath.NewIntFromString("100000000000000000000000") // 100k +) + +// configureAndInitChain initializes the network with the given configuration. +// It creates the genesis state and starts the network. +func (n *IntegrationNetwork) configureAndInitChain() error { + // Create validator set with the amount of validators specified in the config + // with the default power of 1. + valSet, valSigners := createValidatorSetAndSigners(n.cfg.AmountOfValidators) + totalBonded := DefaultBondedAmount.Mul(sdkmath.NewInt(int64(n.cfg.AmountOfValidators))) + + valFlags := make([]cmtproto.BlockIDFlag, len(valSet.Validators)) + for i := range valSet.Validators { + valFlags[i] = cmtproto.BlockIDFlagCommit + } + + // Build staking type validators and delegations + validators, err := createStakingValidators(valSet.Validators, DefaultBondedAmount, n.cfg.OperatorsAddrs) + if err != nil { + return err + } + + // Create genesis accounts and funded balances based on the config + genAccounts, fundedAccountBalances := getGenAccountsAndBalances(n.cfg, validators) + + fundedAccountBalances = addBondedModuleAccountToFundedBalances( + fundedAccountBalances, + sdktypes.NewCoin(n.cfg.BondDenom, totalBonded), + ) + + delegations := createDelegations(validators) + + // Create a new EvmosApp with the following params + exrpApp := exrpcommon.CreateExrpApp(n.cfg.ChainID, n.cfg.CustomBaseAppOpts...) + + stakingParams := StakingCustomGenesisState{ + denom: n.cfg.BondDenom, + maxValidators: n.cfg.MaxValidators, + validators: validators, + delegations: delegations, + } + govParams := GovCustomGenesisState{ + denom: n.cfg.Denom, + minDepositAmt: n.cfg.MinDepositAmt, + } + + totalSupply := calculateTotalSupply(fundedAccountBalances) + bankParams := BankCustomGenesisState{ + totalSupply: totalSupply, + balances: fundedAccountBalances, + } + + // Get the corresponding slashing info and missed block info + // for the created validators + slashingParams, err := getValidatorsSlashingGen(validators, exrpApp.StakingKeeper) + if err != nil { + return err + } + + // Configure Genesis state + genesisState := newDefaultGenesisState( + exrpApp, + defaultGenesisParams{ + genAccounts: genAccounts, + staking: stakingParams, + bank: bankParams, + slashing: slashingParams, + gov: govParams, + }, + ) + + // modify genesis state if there're any custom genesis state + // for specific modules + genesisState, err = customizeGenesis(exrpApp, n.cfg.CustomGenesisState, genesisState) + if err != nil { + return err + } + + // Init chain + stateBytes, err := cmtjson.MarshalIndent(genesisState, "", " ") + if err != nil { + return err + } + + // Consensus module does not have a genesis state on the app, + // but can customize the consensus parameters of the chain on initialization + consensusParams := exrpcommon.DefaultConsensusParams + if gen, ok := n.cfg.CustomGenesisState[consensustypes.ModuleName]; ok { + consensusParams, ok = gen.(*cmtproto.ConsensusParams) + if !ok { + return fmt.Errorf("invalid type for consensus parameters. Expected: cmtproto.ConsensusParams, got %T", gen) + } + } + + now := time.Now().UTC() + if _, err := exrpApp.InitChain( + &abcitypes.RequestInitChain{ + Time: now, + ChainId: n.cfg.ChainID, + Validators: []abcitypes.ValidatorUpdate{}, + ConsensusParams: consensusParams, + AppStateBytes: stateBytes, + }, + ); err != nil { + return err + } + + header := cmtproto.Header{ + ChainID: n.cfg.ChainID, + Height: exrpApp.LastBlockHeight() + 1, + AppHash: exrpApp.LastCommitID().Hash, + Time: now, + ValidatorsHash: valSet.Hash(), + NextValidatorsHash: valSet.Hash(), + ProposerAddress: valSet.Proposer.Address, + Version: tmversion.Consensus{ + Block: version.BlockProtocol, + }, + } + + req := BuildFinalizeBlockReq(header, valSet.Validators, valFlags, nil) + if _, err := exrpApp.FinalizeBlock(req); err != nil { + return err + } + + // TODO - this might not be the best way to initilize the context + n.ctx = exrpApp.BaseApp.NewContextLegacy(false, header) + + // Commit genesis changes + if _, err := exrpApp.Commit(); err != nil { + return err + } + + // Set networks global parameters + var blockMaxGas uint64 = math.MaxUint64 + if consensusParams.Block != nil && consensusParams.Block.MaxGas > 0 { + blockMaxGas = uint64(consensusParams.Block.MaxGas) //nolint:gosec // G115 + } + + n.app = exrpApp + n.ctx = n.ctx.WithConsensusParams(*consensusParams) + n.ctx = n.ctx.WithBlockGasMeter(types.NewInfiniteGasMeterWithLimit(blockMaxGas)) + + n.validators = validators + n.valSet = valSet + n.valFlags = valFlags + n.valSigners = valSigners + + return nil +} + +// GetContext returns the network's context +func (n *IntegrationNetwork) GetContext() sdktypes.Context { + return n.ctx +} + +// WithIsCheckTxCtx switches the network's checkTx property +func (n *IntegrationNetwork) WithIsCheckTxCtx(isCheckTx bool) sdktypes.Context { + n.ctx = n.ctx.WithIsCheckTx(isCheckTx) + return n.ctx +} + +// GetChainID returns the network's chainID +func (n *IntegrationNetwork) GetChainID() string { + return n.cfg.ChainID +} + +// GetEIP155ChainID returns the network EIp-155 chainID number +func (n *IntegrationNetwork) GetEIP155ChainID() *big.Int { + return n.cfg.EIP155ChainID +} + +// GetValidatorSet returns the network's validator set +func (n *IntegrationNetwork) GetValidatorSet() *cmttypes.ValidatorSet { + return n.valSet +} + +// GetValidatorSigners returns the network's validator signers +func (n *IntegrationNetwork) GetValidatorSigners() map[string]cmttypes.PrivValidator { + return n.valSigners +} + +// GetMinDepositAmt returns the network's min deposit amount +func (n *IntegrationNetwork) GetMinDepositAmt() sdkmath.Int { + return n.cfg.MinDepositAmt +} + +// GetChainConfig returns the network's chain config +func (n *IntegrationNetwork) GetEVMChainConfig() *gethparams.ChainConfig { + params := n.app.EvmKeeper.GetParams(n.ctx) + return params.ChainConfig.EthereumConfig(n.cfg.EIP155ChainID) +} + +// GetDenom returns the network's denom +func (n *IntegrationNetwork) GetDenom() string { + return n.cfg.Denom +} + +// GetBondDenom returns the network's bond denom +func (n *IntegrationNetwork) GetBondDenom() string { + return n.cfg.BondDenom +} + +// GetOtherDenoms returns network's other supported denoms +func (n *IntegrationNetwork) GetOtherDenoms() []string { + return n.cfg.OtherCoinDenom +} + +// GetValidators returns the network's validators +func (n *IntegrationNetwork) GetValidators() []stakingtypes.Validator { + return n.validators +} + +// GetOtherDenoms returns network's other supported denoms +func (n *IntegrationNetwork) GetEncodingConfig() sdktestutil.TestEncodingConfig { + return sdktestutil.TestEncodingConfig{ + InterfaceRegistry: n.app.InterfaceRegistry(), + Codec: n.app.AppCodec(), + TxConfig: n.app.GetTxConfig(), + Amino: n.app.LegacyAmino(), + } +} + +// BroadcastTxSync broadcasts the given txBytes to the network and returns the response. +// TODO - this should be change to gRPC +func (n *IntegrationNetwork) BroadcastTxSync(txBytes []byte) (abcitypes.ExecTxResult, error) { + header := n.ctx.BlockHeader() + // Update block header and BeginBlock + header.Height++ + header.AppHash = n.app.LastCommitID().Hash + // Calculate new block time after duration + newBlockTime := header.Time.Add(time.Second) + header.Time = newBlockTime + + req := BuildFinalizeBlockReq(header, n.valSet.Validators, n.valFlags, nil, txBytes) + + // dont include the DecidedLastCommit because we're not committing the changes + // here, is just for broadcasting the tx. To persist the changes, use the + // NextBlock or NextBlockAfter functions + req.DecidedLastCommit = abcitypes.CommitInfo{} + + blockRes, err := n.app.BaseApp.FinalizeBlock(req) + if err != nil { + return abcitypes.ExecTxResult{}, err + } + if len(blockRes.TxResults) != 1 { + return abcitypes.ExecTxResult{}, fmt.Errorf("unexpected number of tx results. Expected 1, got: %d", len(blockRes.TxResults)) + } + return *blockRes.TxResults[0], nil +} + +// Simulate simulates the given txBytes to the network and returns the simulated response. +// TODO - this should be change to gRPC +func (n *IntegrationNetwork) Simulate(txBytes []byte) (*txtypes.SimulateResponse, error) { + gas, result, err := n.app.BaseApp.Simulate(txBytes) + if err != nil { + return nil, err + } + return &txtypes.SimulateResponse{ + GasInfo: &gas, + Result: result, + }, nil +} + +// CheckTx calls the BaseApp's CheckTx method with the given txBytes to the network and returns the response. +func (n *IntegrationNetwork) CheckTx(txBytes []byte) (*abcitypes.ResponseCheckTx, error) { + req := &abcitypes.RequestCheckTx{Tx: txBytes} + res, err := n.app.BaseApp.CheckTx(req) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/testutil/integration/exrp/integration/setup.go b/testutil/integration/exrp/integration/setup.go new file mode 100644 index 0000000..630c68d --- /dev/null +++ b/testutil/integration/exrp/integration/setup.go @@ -0,0 +1,475 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package exrpintegration + +import ( + "fmt" + "slices" + "time" + + "github.com/cosmos/gogoproto/proto" + + capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" + + sdkmath "cosmossdk.io/math" + cmttypes "github.com/cometbft/cometbft/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/testutil/mock" + sdktypes "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/crypto" + + evmostypes "github.com/evmos/evmos/v20/types" + epochstypes "github.com/evmos/evmos/v20/x/epochs/types" + erc20types "github.com/evmos/evmos/v20/x/erc20/types" + feemarkettypes "github.com/evmos/evmos/v20/x/feemarket/types" + infltypes "github.com/evmos/evmos/v20/x/inflation/v1/types" + + evmtypes "github.com/evmos/evmos/v20/x/evm/types" + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" + + "github.com/xrplevm/node/v6/app" +) + +// genSetupFn is the type for the module genesis setup functions +type genSetupFn func(app *app.App, genesisState evmostypes.GenesisState, customGenesis interface{}) (evmostypes.GenesisState, error) + +// defaultGenesisParams contains the params that are needed to +// setup the default genesis for the testing setup +type defaultGenesisParams struct { + genAccounts []authtypes.GenesisAccount + staking StakingCustomGenesisState + slashing SlashingCustomGenesisState + bank BankCustomGenesisState + gov GovCustomGenesisState +} + +// genesisSetupFunctions contains the available genesis setup functions +// that can be used to customize the network genesis +var genesisSetupFunctions = map[string]genSetupFn{ + evmtypes.ModuleName: genStateSetter[*evmtypes.GenesisState](evmtypes.ModuleName), + erc20types.ModuleName: genStateSetter[*erc20types.GenesisState](erc20types.ModuleName), + govtypes.ModuleName: genStateSetter[*govtypesv1.GenesisState](govtypes.ModuleName), + infltypes.ModuleName: genStateSetter[*infltypes.GenesisState](infltypes.ModuleName), + feemarkettypes.ModuleName: genStateSetter[*feemarkettypes.GenesisState](feemarkettypes.ModuleName), + distrtypes.ModuleName: genStateSetter[*distrtypes.GenesisState](distrtypes.ModuleName), + banktypes.ModuleName: setBankGenesisState, + authtypes.ModuleName: setAuthGenesisState, + epochstypes.ModuleName: genStateSetter[*epochstypes.GenesisState](epochstypes.ModuleName), + consensustypes.ModuleName: func(_ *app.App, genesisState evmostypes.GenesisState, _ interface{}) (evmostypes.GenesisState, error) { + // no-op. Consensus does not have a genesis state on the application + // but the params are used on it + // (e.g. block max gas, max bytes). + // This is handled accordingly on chain and context initialization + return genesisState, nil + }, + capabilitytypes.ModuleName: genStateSetter[*capabilitytypes.GenesisState](capabilitytypes.ModuleName), +} + +// genStateSetter is a generic function to set module-specific genesis state +func genStateSetter[T proto.Message](moduleName string) genSetupFn { + return func(app *app.App, genesisState evmostypes.GenesisState, customGenesis interface{}) (evmostypes.GenesisState, error) { + moduleGenesis, ok := customGenesis.(T) + if !ok { + return nil, fmt.Errorf("invalid type %T for %s module genesis state", customGenesis, moduleName) + } + + genesisState[moduleName] = app.AppCodec().MustMarshalJSON(moduleGenesis) + return genesisState, nil + } +} + +// createValidatorSetAndSigners creates validator set with the amount of validators specified +// with the default power of 1. +func createValidatorSetAndSigners(numberOfValidators int) (*cmttypes.ValidatorSet, map[string]cmttypes.PrivValidator) { + // Create validator set + tmValidators := make([]*cmttypes.Validator, 0, numberOfValidators) + signers := make(map[string]cmttypes.PrivValidator, numberOfValidators) + + for i := 0; i < numberOfValidators; i++ { + privVal := mock.NewPV() + pubKey, _ := privVal.GetPubKey() + validator := cmttypes.NewValidator(pubKey, 1) + tmValidators = append(tmValidators, validator) + signers[pubKey.Address().String()] = privVal + } + + return cmttypes.NewValidatorSet(tmValidators), signers +} + +// createGenesisAccounts returns a slice of genesis accounts from the given +// account addresses. +func createGenesisAccounts(accounts []sdktypes.AccAddress) []authtypes.GenesisAccount { + numberOfAccounts := len(accounts) + genAccounts := make([]authtypes.GenesisAccount, 0, numberOfAccounts) + emptyCodeHash := crypto.Keccak256Hash(nil).String() + for _, acc := range accounts { + baseAcc := authtypes.NewBaseAccount(acc, nil, 0, 0) + ethAcc := &evmostypes.EthAccount{ + BaseAccount: baseAcc, + CodeHash: emptyCodeHash, + } + genAccounts = append(genAccounts, ethAcc) + } + return genAccounts +} + +// getAccAddrsFromBalances returns a slice of genesis accounts from the +// given balances. +func getAccAddrsFromBalances(balances []banktypes.Balance) []sdktypes.AccAddress { + numberOfBalances := len(balances) + genAccounts := make([]sdktypes.AccAddress, 0, numberOfBalances) + for _, balance := range balances { + genAccounts = append(genAccounts, sdktypes.AccAddress(balance.Address)) + } + return genAccounts +} + +// createBalances creates balances for the given accounts and coin +func createBalances(accounts []sdktypes.AccAddress, denoms []string) []banktypes.Balance { + slices.Sort(denoms) + numberOfAccounts := len(accounts) + coins := make([]sdktypes.Coin, len(denoms)) + for i, denom := range denoms { + coins[i] = sdktypes.NewCoin(denom, PrefundedAccountInitialBalance) + } + fundedAccountBalances := make([]banktypes.Balance, 0, numberOfAccounts) + for _, acc := range accounts { + balance := banktypes.Balance{ + Address: acc.String(), + Coins: coins, + } + + fundedAccountBalances = append(fundedAccountBalances, balance) + } + return fundedAccountBalances +} + +// createStakingValidator creates a staking validator from the given tm validator and bonded +func createStakingValidator(val *cmttypes.Validator, bondedAmt sdkmath.Int, operatorAddr *sdktypes.AccAddress) (stakingtypes.Validator, error) { + pk, err := cryptocodec.FromCmtPubKeyInterface(val.PubKey) + if err != nil { + return stakingtypes.Validator{}, err + } + + pkAny, err := codectypes.NewAnyWithValue(pk) + if err != nil { + return stakingtypes.Validator{}, err + } + + opAddr := sdktypes.ValAddress(val.Address).String() + if operatorAddr != nil { + opAddr = sdktypes.ValAddress(operatorAddr.Bytes()).String() + } + + // Default to 5% commission + commission := stakingtypes.NewCommission(sdkmath.LegacyNewDecWithPrec(5, 2), sdkmath.LegacyNewDecWithPrec(2, 1), sdkmath.LegacyNewDecWithPrec(5, 2)) + validator := stakingtypes.Validator{ + OperatorAddress: opAddr, + ConsensusPubkey: pkAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: bondedAmt, + DelegatorShares: sdktypes.DefaultPowerReduction.ToLegacyDec(), + Description: stakingtypes.Description{}, + UnbondingHeight: int64(0), + UnbondingTime: time.Unix(0, 0).UTC(), + Commission: commission, + MinSelfDelegation: sdkmath.ZeroInt(), + } + return validator, nil +} + +// createStakingValidators creates staking validators from the given tm validators and bonded +// amounts +func createStakingValidators(tmValidators []*cmttypes.Validator, bondedAmt sdkmath.Int, operatorsAddresses []sdktypes.AccAddress) ([]stakingtypes.Validator, error) { + if len(operatorsAddresses) == 0 { + return createStakingValidatorsWithRandomOperator(tmValidators, bondedAmt) + } + return createStakingValidatorsWithSpecificOperator(tmValidators, bondedAmt, operatorsAddresses) +} + +// createStakingValidatorsWithRandomOperator creates staking validators with non-specified operator addresses. +func createStakingValidatorsWithRandomOperator(tmValidators []*cmttypes.Validator, bondedAmt sdkmath.Int) ([]stakingtypes.Validator, error) { + amountOfValidators := len(tmValidators) + stakingValidators := make([]stakingtypes.Validator, 0, amountOfValidators) + for _, val := range tmValidators { + validator, err := createStakingValidator(val, bondedAmt, nil) + if err != nil { + return nil, err + } + stakingValidators = append(stakingValidators, validator) + } + return stakingValidators, nil +} + +// createStakingValidatorsWithSpecificOperator creates staking validators with the given operator addresses. +func createStakingValidatorsWithSpecificOperator(tmValidators []*cmttypes.Validator, bondedAmt sdkmath.Int, operatorsAddresses []sdktypes.AccAddress) ([]stakingtypes.Validator, error) { + amountOfValidators := len(tmValidators) + stakingValidators := make([]stakingtypes.Validator, 0, amountOfValidators) + operatorsCount := len(operatorsAddresses) + if operatorsCount != amountOfValidators { + panic(fmt.Sprintf("provided %d validator operator keys but need %d!", operatorsCount, amountOfValidators)) + } + for i, val := range tmValidators { + validator, err := createStakingValidator(val, bondedAmt, &operatorsAddresses[i]) + if err != nil { + return nil, err + } + stakingValidators = append(stakingValidators, validator) + } + return stakingValidators, nil +} + +// createDelegations creates delegations for the given validators and account +func createDelegations(validators []stakingtypes.Validator) []stakingtypes.Delegation { + amountOfValidators := len(validators) + delegations := make([]stakingtypes.Delegation, 0, amountOfValidators) + for _, val := range validators { + valAddr, err := sdktypes.ValAddressFromBech32(val.OperatorAddress) + if err != nil { + panic(err) + } + delegation := stakingtypes.NewDelegation(sdktypes.AccAddress(valAddr).String(), val.OperatorAddress, sdktypes.DefaultPowerReduction.ToLegacyDec()) + delegations = append(delegations, delegation) + } + return delegations +} + +// getValidatorsSlashingGen creates the validators signingInfos and missedBlocks +// records necessary for the slashing module genesis +func getValidatorsSlashingGen(validators []stakingtypes.Validator, sk slashingtypes.StakingKeeper) (SlashingCustomGenesisState, error) { + valCount := len(validators) + signInfo := make([]slashingtypes.SigningInfo, valCount) + missedBlocks := make([]slashingtypes.ValidatorMissedBlocks, valCount) + for i, val := range validators { + consAddrBz, err := val.GetConsAddr() + if err != nil { + return SlashingCustomGenesisState{}, err + } + consAddr, err := sk.ConsensusAddressCodec().BytesToString(consAddrBz) + if err != nil { + return SlashingCustomGenesisState{}, err + } + signInfo[i] = slashingtypes.SigningInfo{ + Address: consAddr, + ValidatorSigningInfo: slashingtypes.ValidatorSigningInfo{ + Address: consAddr, + }, + } + missedBlocks[i] = slashingtypes.ValidatorMissedBlocks{ + Address: consAddr, + } + } + return SlashingCustomGenesisState{ + signingInfo: signInfo, + missedBlocks: missedBlocks, + }, nil +} + +// StakingCustomGenesisState defines the staking genesis state +type StakingCustomGenesisState struct { + denom string + maxValidators uint32 + + validators []stakingtypes.Validator + delegations []stakingtypes.Delegation +} + +// setDefaultStakingGenesisState sets the default staking genesis state +func setDefaultStakingGenesisState(app *app.App, genesisState evmostypes.GenesisState, overwriteParams StakingCustomGenesisState) evmostypes.GenesisState { + // Set staking params + stakingParams := stakingtypes.DefaultParams() + stakingParams.BondDenom = overwriteParams.denom + stakingParams.MaxValidators = overwriteParams.maxValidators + + stakingGenesis := stakingtypes.NewGenesisState( + stakingParams, + overwriteParams.validators, + overwriteParams.delegations, + ) + genesisState[stakingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(stakingGenesis) + return genesisState +} + +type BankCustomGenesisState struct { + totalSupply sdktypes.Coins + balances []banktypes.Balance +} + +// setDefaultBankGenesisState sets the default bank genesis state +func setDefaultBankGenesisState(app *app.App, genesisState evmostypes.GenesisState, overwriteParams BankCustomGenesisState) evmostypes.GenesisState { + bankGenesis := banktypes.NewGenesisState( + banktypes.DefaultGenesisState().Params, + overwriteParams.balances, + overwriteParams.totalSupply, + []banktypes.Metadata{}, + []banktypes.SendEnabled{}, + ) + genesisState[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGenesis) + return genesisState +} + +// SlashingCustomGenesisState defines the corresponding +// validators signing info and missed blocks for the genesis state +type SlashingCustomGenesisState struct { + signingInfo []slashingtypes.SigningInfo + missedBlocks []slashingtypes.ValidatorMissedBlocks +} + +// setDefaultSlashingGenesisState sets the default slashing genesis state +func setDefaultSlashingGenesisState(app *app.App, genesisState evmostypes.GenesisState, overwriteParams SlashingCustomGenesisState) evmostypes.GenesisState { + slashingGen := slashingtypes.DefaultGenesisState() + slashingGen.SigningInfos = overwriteParams.signingInfo + slashingGen.MissedBlocks = overwriteParams.missedBlocks + + slashingGen.Params.SlashFractionDoubleSign = sdkmath.LegacyZeroDec() + slashingGen.Params.SlashFractionDowntime = sdkmath.LegacyZeroDec() + + genesisState[slashingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(slashingGen) + return genesisState +} + +// setBankGenesisState updates the bank genesis state with custom genesis state +func setBankGenesisState(app *app.App, genesisState evmostypes.GenesisState, customGenesis interface{}) (evmostypes.GenesisState, error) { + customGen, ok := customGenesis.(*banktypes.GenesisState) + if !ok { + return nil, fmt.Errorf("invalid type %T for bank module genesis state", customGenesis) + } + + bankGen := &banktypes.GenesisState{} + app.AppCodec().MustUnmarshalJSON(genesisState[banktypes.ModuleName], bankGen) + + if len(customGen.Balances) > 0 { + coins := sdktypes.NewCoins() + bankGen.Balances = append(bankGen.Balances, customGen.Balances...) + for _, b := range customGen.Balances { + coins = append(coins, b.Coins...) + } + bankGen.Supply = bankGen.Supply.Add(coins...) + } + if len(customGen.DenomMetadata) > 0 { + bankGen.DenomMetadata = append(bankGen.DenomMetadata, customGen.DenomMetadata...) + } + + if len(customGen.SendEnabled) > 0 { + bankGen.SendEnabled = append(bankGen.SendEnabled, customGen.SendEnabled...) + } + + bankGen.Params = customGen.Params + + genesisState[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGen) + return genesisState, nil +} + +// calculateTotalSupply calculates the total supply from the given balances +func calculateTotalSupply(fundedAccountsBalances []banktypes.Balance) sdktypes.Coins { + totalSupply := sdktypes.NewCoins() + for _, balance := range fundedAccountsBalances { + totalSupply = totalSupply.Add(balance.Coins...) + } + return totalSupply +} + +// addBondedModuleAccountToFundedBalances adds bonded amount to bonded pool module account and include it on funded accounts +func addBondedModuleAccountToFundedBalances( + fundedAccountsBalances []banktypes.Balance, + totalBonded sdktypes.Coin, +) []banktypes.Balance { + return append(fundedAccountsBalances, banktypes.Balance{ + Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(), + Coins: sdktypes.Coins{totalBonded}, + }) +} + +// setDefaultAuthGenesisState sets the default auth genesis state +func setDefaultAuthGenesisState(app *app.App, genesisState evmostypes.GenesisState, genAccs []authtypes.GenesisAccount) evmostypes.GenesisState { + defaultAuthGen := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs) + genesisState[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(defaultAuthGen) + return genesisState +} + +// setAuthGenesisState updates the bank genesis state with custom genesis state +func setAuthGenesisState(app *app.App, genesisState evmostypes.GenesisState, customGenesis interface{}) (evmostypes.GenesisState, error) { + customGen, ok := customGenesis.(*authtypes.GenesisState) + if !ok { + return nil, fmt.Errorf("invalid type %T for auth module genesis state", customGenesis) + } + + authGen := &authtypes.GenesisState{} + app.AppCodec().MustUnmarshalJSON(genesisState[authtypes.ModuleName], authGen) + + if len(customGen.Accounts) > 0 { + authGen.Accounts = append(authGen.Accounts, customGen.Accounts...) + } + + authGen.Params = customGen.Params + + genesisState[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(authGen) + return genesisState, nil +} + +// GovCustomGenesisState defines the gov genesis state +type GovCustomGenesisState struct { + denom string + minDepositAmt sdkmath.Int +} + +// setDefaultGovGenesisState sets the default gov genesis state +func setDefaultGovGenesisState(app *app.App, genesisState evmostypes.GenesisState, overwriteParams GovCustomGenesisState) evmostypes.GenesisState { + govGen := govtypesv1.DefaultGenesisState() + updatedParams := govGen.Params + minDepositAmt := overwriteParams.minDepositAmt + updatedParams.MinDeposit = sdktypes.NewCoins(sdktypes.NewCoin(overwriteParams.denom, minDepositAmt)) + updatedParams.ExpeditedMinDeposit = sdktypes.NewCoins(sdktypes.NewCoin(overwriteParams.denom, minDepositAmt.MulRaw(2))) + govGen.Params = updatedParams + genesisState[govtypes.ModuleName] = app.AppCodec().MustMarshalJSON(govGen) + return genesisState +} + +func setDefaultErc20GenesisState(app *app.App, genesisState evmostypes.GenesisState) evmostypes.GenesisState { + erc20Gen := erc20types.DefaultGenesisState() + genesisState[erc20types.ModuleName] = app.AppCodec().MustMarshalJSON(erc20Gen) + return genesisState +} + +// defaultAuthGenesisState sets the default genesis state +// for the testing setup +func newDefaultGenesisState(app *app.App, params defaultGenesisParams) evmostypes.GenesisState { + genesisState := app.DefaultGenesis() + + genesisState = setDefaultAuthGenesisState(app, genesisState, params.genAccounts) + genesisState = setDefaultStakingGenesisState(app, genesisState, params.staking) + genesisState = setDefaultBankGenesisState(app, genesisState, params.bank) + genesisState = setDefaultGovGenesisState(app, genesisState, params.gov) + genesisState = setDefaultSlashingGenesisState(app, genesisState, params.slashing) + genesisState = setDefaultErc20GenesisState(app, genesisState) + + return genesisState +} + +// customizeGenesis modifies genesis state if there're any custom genesis state +// for specific modules +func customizeGenesis(app *app.App, customGen exrpcommon.CustomGenesisState, genesisState evmostypes.GenesisState) (evmostypes.GenesisState, error) { + var err error + for mod, modGenState := range customGen { + if fn, found := genesisSetupFunctions[mod]; found { + genesisState, err = fn(app, genesisState, modGenState) + if err != nil { + return genesisState, err + } + } else { + panic(fmt.Sprintf("module %s not found in genesis setup functions", mod)) + } + } + return genesisState, err +} diff --git a/testutil/integration/exrp/integration/unit_network.go b/testutil/integration/exrp/integration/unit_network.go new file mode 100644 index 0000000..74af9a1 --- /dev/null +++ b/testutil/integration/exrp/integration/unit_network.go @@ -0,0 +1,57 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package exrpintegration + +import ( + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/evmos/evmos/v20/x/evm/statedb" + inflationtypes "github.com/evmos/evmos/v20/x/inflation/v1/types" + "github.com/xrplevm/node/v6/app" + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" +) + +// UnitTestIntegrationNetwork is the implementation of the Network interface for unit tests. +// It embeds the IntegrationNetwork struct to reuse its methods and +// makes the App public for easier testing. +type UnitTestIntegrationNetwork struct { + IntegrationNetwork + App *app.App +} + +var _ Network = (*UnitTestIntegrationNetwork)(nil) + +// NewUnitTestNetwork configures and initializes a new Evmos Network instance with +// the given configuration options. If no configuration options are provided +// it uses the default configuration. +// +// It panics if an error occurs. +// Note: Only uses for Unit Tests +func NewUnitTestNetwork(opts ...exrpcommon.ConfigOption) *UnitTestIntegrationNetwork { + network := New(opts...) + return &UnitTestIntegrationNetwork{ + IntegrationNetwork: *network, + App: network.app, + } +} + +// GetStateDB returns the state database for the current block. +func (n *UnitTestIntegrationNetwork) GetStateDB() *statedb.StateDB { + headerHash := n.GetContext().HeaderHash() + return statedb.New( + n.GetContext(), + n.App.EvmKeeper, + statedb.NewEmptyTxConfig(common.BytesToHash(headerHash)), + ) +} + +// FundAccount funds the given account with the given amount of coins. +func (n *UnitTestIntegrationNetwork) FundAccount(addr sdktypes.AccAddress, coins sdktypes.Coins) error { + ctx := n.GetContext() + + if err := n.app.BankKeeper.MintCoins(ctx, inflationtypes.ModuleName, coins); err != nil { + return err + } + + return n.app.BankKeeper.SendCoinsFromModuleToAccount(ctx, inflationtypes.ModuleName, addr, coins) +} diff --git a/testutil/integration/exrp/upgrade/abci.go b/testutil/integration/exrp/upgrade/abci.go new file mode 100644 index 0000000..b3a82c9 --- /dev/null +++ b/testutil/integration/exrp/upgrade/abci.go @@ -0,0 +1,101 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package exrpupgrade + +import ( + "time" + + storetypes "cosmossdk.io/store/types" + abcitypes "github.com/cometbft/cometbft/abci/types" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" +) + +// NextBlock is a private helper function that runs the EndBlocker logic, commits the changes, +// updates the header and runs the BeginBlocker +func (n *UpgradeIntegrationNetwork) NextBlock() error { + return n.NextBlockAfter(time.Second) +} + +// NextBlockAfter is a private helper function that runs the FinalizeBlock logic, updates the context and +// commits the changes to have a block time after the given duration. +func (n *UpgradeIntegrationNetwork) NextBlockAfter(duration time.Duration) error { + _, err := n.finalizeBlockAndCommit(duration) + return err +} + +// NextBlockWithTxs is a helper function that runs the FinalizeBlock logic +// with the provided tx bytes, updates the context and +// commits the changes to have a block time after the given duration. +func (n *UpgradeIntegrationNetwork) NextBlockWithTxs(txBytes ...[]byte) (*abcitypes.ResponseFinalizeBlock, error) { + return n.finalizeBlockAndCommit(time.Second, txBytes...) +} + +// finalizeBlockAndCommit is a private helper function that runs the FinalizeBlock logic +// with the provided txBytes, updates the context and +// commits the changes to have a block time after the given duration. +func (n *UpgradeIntegrationNetwork) finalizeBlockAndCommit(duration time.Duration, txBytes ...[]byte) (*abcitypes.ResponseFinalizeBlock, error) { + header := n.ctx.BlockHeader() + // Update block header and BeginBlock + header.Height++ + header.AppHash = n.app.LastCommitID().Hash + // Calculate new block time after duration + newBlockTime := header.Time.Add(duration) + header.Time = newBlockTime + + // FinalizeBlock to run endBlock, deliverTx & beginBlock logic + req := BuildFinalizeBlockReq(header, n.valSet.Validators, txBytes...) + + res, err := n.app.FinalizeBlock(req) + if err != nil { + return nil, err + } + + newCtx := n.app.BaseApp.NewContextLegacy(false, header) + + // Update context header + newCtx = newCtx.WithMinGasPrices(n.ctx.MinGasPrices()) + newCtx = newCtx.WithKVGasConfig(n.ctx.KVGasConfig()) + newCtx = newCtx.WithTransientKVGasConfig(n.ctx.TransientKVGasConfig()) + newCtx = newCtx.WithConsensusParams(n.ctx.ConsensusParams()) + // This might have to be changed with time if we want to test gas limits + newCtx = newCtx.WithBlockGasMeter(storetypes.NewInfiniteGasMeter()) + newCtx = newCtx.WithVoteInfos(req.DecidedLastCommit.GetVotes()) + n.ctx = newCtx + + // commit changes + _, err = n.app.Commit() + + return res, err +} + +// buildFinalizeBlockReq is a helper function to build +// properly the FinalizeBlock request +func BuildFinalizeBlockReq(header cmtproto.Header, validators []*cmttypes.Validator, txs ...[]byte) *abcitypes.RequestFinalizeBlock { + // add validator's commit info to allocate corresponding tokens to validators + ci := GetCommitInfo(validators) + return &abcitypes.RequestFinalizeBlock{ + Misbehavior: nil, + Height: header.Height, + DecidedLastCommit: ci, + Hash: header.AppHash, + NextValidatorsHash: header.ValidatorsHash, + ProposerAddress: header.ProposerAddress, + Time: header.Time, + Txs: txs, + } +} + +func GetCommitInfo(validators []*cmttypes.Validator) abcitypes.CommitInfo { + voteInfos := make([]abcitypes.VoteInfo, len(validators)) + for i, val := range validators { + voteInfos[i] = abcitypes.VoteInfo{ + Validator: abcitypes.Validator{ + Address: val.Address, + Power: val.VotingPower, + }, + BlockIdFlag: cmtproto.BlockIDFlagCommit, + } + } + return abcitypes.CommitInfo{Votes: voteInfos} +} diff --git a/testutil/integration/exrp/upgrade/config.go b/testutil/integration/exrp/upgrade/config.go new file mode 100644 index 0000000..524423f --- /dev/null +++ b/testutil/integration/exrp/upgrade/config.go @@ -0,0 +1,25 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package exrpupgrade + +import ( + "os" + + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" +) + +func DefaultUpgradeConfig() exrpcommon.Config { + return exrpcommon.DefaultConfig() +} + +// WithGenesisFile sets the genesis file for the network. +func WithGenesisFile(genesisFile string) exrpcommon.ConfigOption { + return func(cfg *exrpcommon.Config) { + genesisBytes, err := os.ReadFile(genesisFile) + if err != nil { + panic(err) + } + cfg.GenesisBytes = genesisBytes + } +} diff --git a/testutil/integration/exrp/upgrade/ibc.go b/testutil/integration/exrp/upgrade/ibc.go new file mode 100644 index 0000000..49be088 --- /dev/null +++ b/testutil/integration/exrp/upgrade/ibc.go @@ -0,0 +1,28 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package exrpupgrade + +import ( + "testing" + + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +// GetIBCChain returns a TestChain instance for the given network. +// Note: the sender accounts are not populated. Do not use this accounts to send transactions during tests. +// The keyring should be used instead. +func (n *UpgradeIntegrationNetwork) GetIBCChain(t *testing.T, coord *ibctesting.Coordinator) *ibctesting.TestChain { + return &ibctesting.TestChain{ + TB: t, + Coordinator: coord, + ChainID: n.GetChainID(), + App: n.app, + CurrentHeader: n.ctx.BlockHeader(), + QueryServer: n.app.GetIBCKeeper(), + TxConfig: n.app.GetTxConfig(), + Codec: n.app.AppCodec(), + Vals: n.valSet, + NextVals: n.valSet, + Signers: n.valSigners, + } +} diff --git a/testutil/integration/exrp/upgrade/keepers.go b/testutil/integration/exrp/upgrade/keepers.go new file mode 100644 index 0000000..bf0d139 --- /dev/null +++ b/testutil/integration/exrp/upgrade/keepers.go @@ -0,0 +1,60 @@ +package exrpupgrade + +import ( + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + + erc20keeper "github.com/evmos/evmos/v20/x/erc20/keeper" + evmkeeper "github.com/evmos/evmos/v20/x/evm/keeper" + feemarketkeeper "github.com/evmos/evmos/v20/x/feemarket/keeper" + poakeeper "github.com/xrplevm/node/v6/x/poa/keeper" +) + +func (n *UpgradeIntegrationNetwork) BankKeeper() bankkeeper.Keeper { + return n.app.BankKeeper +} + +func (n *UpgradeIntegrationNetwork) ERC20Keeper() erc20keeper.Keeper { + return n.app.Erc20Keeper +} + +func (n *UpgradeIntegrationNetwork) EvmKeeper() evmkeeper.Keeper { + return *n.app.EvmKeeper +} + +func (n *UpgradeIntegrationNetwork) GovKeeper() *govkeeper.Keeper { + return &n.app.GovKeeper +} + +func (n *UpgradeIntegrationNetwork) StakingKeeper() *stakingkeeper.Keeper { + return n.app.StakingKeeper.Keeper +} + +func (n *UpgradeIntegrationNetwork) SlashingKeeper() slashingkeeper.Keeper { + return n.app.SlashingKeeper +} + +func (n *UpgradeIntegrationNetwork) DistrKeeper() distrkeeper.Keeper { + return n.app.DistrKeeper +} + +func (n *UpgradeIntegrationNetwork) AccountKeeper() authkeeper.AccountKeeper { + return n.app.AccountKeeper +} + +func (n *UpgradeIntegrationNetwork) AuthzKeeper() authzkeeper.Keeper { + return n.app.AuthzKeeper +} + +func (n *UpgradeIntegrationNetwork) FeeMarketKeeper() feemarketkeeper.Keeper { + return n.app.FeeMarketKeeper +} + +func (n *UpgradeIntegrationNetwork) PoaKeeper() poakeeper.Keeper { + return n.app.PoaKeeper +} diff --git a/testutil/integration/exrp/upgrade/network.go b/testutil/integration/exrp/upgrade/network.go new file mode 100644 index 0000000..329f2c4 --- /dev/null +++ b/testutil/integration/exrp/upgrade/network.go @@ -0,0 +1,332 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package exrpupgrade + +import ( + "encoding/json" + "fmt" + "math" + "math/big" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/xrplevm/node/v6/app" + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" + + abcitypes "github.com/cometbft/cometbft/abci/types" + ed25519 "github.com/cometbft/cometbft/crypto/ed25519" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmversion "github.com/cometbft/cometbft/proto/tendermint/version" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/cometbft/cometbft/version" + sdktypes "github.com/cosmos/cosmos-sdk/types" + sdktestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/evmos/evmos/v20/types" +) + +// Network is the interface that wraps the methods to interact with integration test network. +// +// It was designed to avoid users to access module's keepers directly and force integration tests +// to be closer to the real user's behavior. +type Network interface { + exrpcommon.Network + + GetEIP155ChainID() *big.Int +} + +var _ Network = (*UpgradeIntegrationNetwork)(nil) + +// UpgradeIntegrationNetwork is the implementation of the Network interface for integration tests. +type UpgradeIntegrationNetwork struct { + cfg exrpcommon.Config + ctx sdktypes.Context + validators []stakingtypes.Validator + app *app.App + + // This is only needed for IBC chain testing setup + valSet *cmttypes.ValidatorSet + valSigners map[string]cmttypes.PrivValidator +} + +// New configures and initializes a new integration Network instance with +// the given configuration options. If no configuration options are provided +// it uses the default configuration. +// +// It panics if an error occurs. +func New(opts ...exrpcommon.ConfigOption) *UpgradeIntegrationNetwork { + cfg := DefaultUpgradeConfig() + // Modify the default config with the given options + for _, opt := range opts { + opt(&cfg) + } + + ctx := sdktypes.Context{} + network := &UpgradeIntegrationNetwork{ + cfg: cfg, + ctx: ctx, + validators: []stakingtypes.Validator{}, + } + + if cfg.GenesisBytes == nil { + panic("GenesisBytes is nil") + } + err := network.configureAndInitChain() + if err != nil { + panic(err) + } + return network +} + +func getValidatorsAndSignersFromCustomGenesisState( + stakingState stakingtypes.GenesisState, +) ( + *cmttypes.ValidatorSet, + map[string]cmttypes.PrivValidator, + []abcitypes.ValidatorUpdate, error, +) { + genesisValidators := stakingState.Validators + + tmValidators := make([]*cmttypes.Validator, 0, len(genesisValidators)) + validatorsUpdates := make([]abcitypes.ValidatorUpdate, 0, len(genesisValidators)) + valSigners := make(map[string]cmttypes.PrivValidator, len(genesisValidators)) + + // For each validator, we need to get the pubkey and create a new validator + for _, val := range genesisValidators { + pb, err := val.CmtConsPublicKey() + if err != nil { + return nil, nil, nil, err + } + pubKey := ed25519.PubKey(pb.GetEd25519()) + + validator := cmttypes.NewValidator(pubKey, 10000000) + tmValidators = append(tmValidators, validator) + validatorsUpdates = append(validatorsUpdates, abcitypes.ValidatorUpdate{ + PubKey: pb, + Power: val.GetConsensusPower(val.Tokens), + }) + } + + return cmttypes.NewValidatorSet(tmValidators), valSigners, validatorsUpdates, nil +} + +// configureAndInitChain initializes the network with the given configuration. +// It creates the genesis state and starts the network. +func (n *UpgradeIntegrationNetwork) configureAndInitChain() error { + // Create a new EvmosApp with the following params + exrpApp := exrpcommon.CreateExrpApp(n.cfg.ChainID, n.cfg.CustomBaseAppOpts...) + + var genesisState app.GenesisState + err := json.Unmarshal(n.cfg.GenesisBytes, &genesisState) + if err != nil { + return fmt.Errorf("error unmarshalling genesis state: %w", err) + } + + stateBytes, ok := genesisState["app_state"] + if !ok { + return fmt.Errorf("app_state not found in genesis state") + } + + var appState map[string]json.RawMessage + err = json.Unmarshal(genesisState["app_state"], &appState) + if err != nil { + return fmt.Errorf("error unmarshalling app state: %w", err) + } + + var stakingState stakingtypes.GenesisState + err = exrpApp.AppCodec().UnmarshalJSON(appState["staking"], &stakingState) + if err != nil { + return fmt.Errorf("error unmarshalling staking state: %w", err) + } + + valSet, valSigners, _, err := getValidatorsAndSignersFromCustomGenesisState(stakingState) + if err != nil { + return fmt.Errorf("error getting validators and signers from custom genesis state: %w", err) + } + + // Consensus module does not have a genesis state on the app, + // but can customize the consensus parameters of the chain on initialization + + var consensusState map[string]json.RawMessage + err = json.Unmarshal(genesisState["consensus"], &consensusState) + if err != nil { + return fmt.Errorf("error unmarshalling consensus state: %w", err) + } + + var consensusParams *cmtproto.ConsensusParams + err = json.Unmarshal(consensusState["params"], &consensusParams) + if err != nil { + return fmt.Errorf("error unmarshalling consensus params: %w", err) + } + + var initialHeight int64 + if err := json.Unmarshal(genesisState["initial_height"], &initialHeight); err != nil { + return fmt.Errorf("initial_height is not an int64") + } + + now := time.Now().UTC() + if _, err := exrpApp.InitChain( + &abcitypes.RequestInitChain{ + Time: now, + ChainId: n.cfg.ChainID, + Validators: []abcitypes.ValidatorUpdate{}, + ConsensusParams: consensusParams, + AppStateBytes: stateBytes, + InitialHeight: initialHeight, + }, + ); err != nil { + return err + } + + header := cmtproto.Header{ + ChainID: n.cfg.ChainID, + Height: initialHeight, + AppHash: exrpApp.LastCommitID().Hash, + Time: now, + ValidatorsHash: valSet.Hash(), + NextValidatorsHash: valSet.Hash(), + ProposerAddress: valSet.Proposer.Address, + Version: tmversion.Consensus{ + Block: version.BlockProtocol, + }, + } + + req := BuildFinalizeBlockReq(header, valSet.Validators, nil, nil) + if _, err := exrpApp.FinalizeBlock(req); err != nil { + return err + } + + // TODO - this might not be the best way to initilize the context + n.ctx = exrpApp.BaseApp.NewContextLegacy(false, header) + + // Commit genesis changes + if _, err := exrpApp.Commit(); err != nil { + return err + } + + // Set networks global parameters + var blockMaxGas uint64 = math.MaxUint64 + if consensusParams.Block != nil && consensusParams.Block.MaxGas > 0 { + blockMaxGas = uint64(consensusParams.Block.MaxGas) //nolint:gosec // G115 + } + + n.app = exrpApp + n.ctx = n.ctx.WithConsensusParams(*consensusParams) + n.ctx = n.ctx.WithBlockGasMeter(types.NewInfiniteGasMeterWithLimit(blockMaxGas)) + + n.validators = stakingState.Validators + n.valSet = valSet + n.valSigners = valSigners + + return nil +} + +// GetContext returns the network's context +func (n *UpgradeIntegrationNetwork) GetContext() sdktypes.Context { + return n.ctx +} + +// WithIsCheckTxCtx switches the network's checkTx property +func (n *UpgradeIntegrationNetwork) WithIsCheckTxCtx(isCheckTx bool) sdktypes.Context { + n.ctx = n.ctx.WithIsCheckTx(isCheckTx) + return n.ctx +} + +// GetChainID returns the network's chainID +func (n *UpgradeIntegrationNetwork) GetChainID() string { + return n.cfg.ChainID +} + +// GetEIP155ChainID returns the network EIp-155 chainID number +func (n *UpgradeIntegrationNetwork) GetEIP155ChainID() *big.Int { + return n.cfg.EIP155ChainID +} + +// GetDenom returns the network's denom +func (n *UpgradeIntegrationNetwork) GetDenom() string { + return n.cfg.Denom +} + +// GetBondDenom returns the network's bond denom +func (n *UpgradeIntegrationNetwork) GetBondDenom() string { + return n.cfg.BondDenom +} + +// GetOtherDenoms returns network's other supported denoms +func (n *UpgradeIntegrationNetwork) GetOtherDenoms() []string { + return n.cfg.OtherCoinDenom +} + +// GetValidators returns the network's validators +func (n *UpgradeIntegrationNetwork) GetValidators() []stakingtypes.Validator { + return n.validators +} + +// GetMinDepositAmt returns the network's min deposit amount +func (n *UpgradeIntegrationNetwork) GetMinDepositAmt() sdkmath.Int { + return n.cfg.MinDepositAmt +} + +// GetOtherDenoms returns network's other supported denoms +func (n *UpgradeIntegrationNetwork) GetEncodingConfig() sdktestutil.TestEncodingConfig { + return sdktestutil.TestEncodingConfig{ + InterfaceRegistry: n.app.InterfaceRegistry(), + Codec: n.app.AppCodec(), + TxConfig: n.app.GetTxConfig(), + Amino: n.app.LegacyAmino(), + } +} + +// BroadcastTxSync broadcasts the given txBytes to the network and returns the response. +// TODO - this should be change to gRPC +func (n *UpgradeIntegrationNetwork) BroadcastTxSync(txBytes []byte) (abcitypes.ExecTxResult, error) { + header := n.ctx.BlockHeader() + // Update block header and BeginBlock + header.Height++ + header.AppHash = n.app.LastCommitID().Hash + // Calculate new block time after duration + newBlockTime := header.Time.Add(time.Second) + header.Time = newBlockTime + + req := BuildFinalizeBlockReq(header, n.valSet.Validators, txBytes) + + // dont include the DecidedLastCommit because we're not committing the changes + // here, is just for broadcasting the tx. To persist the changes, use the + // NextBlock or NextBlockAfter functions + req.DecidedLastCommit = abcitypes.CommitInfo{} + + blockRes, err := n.app.BaseApp.FinalizeBlock(req) + if err != nil { + return abcitypes.ExecTxResult{}, err + } + if len(blockRes.TxResults) != 1 { + return abcitypes.ExecTxResult{}, fmt.Errorf("unexpected number of tx results. Expected 1, got: %d", len(blockRes.TxResults)) + } + return *blockRes.TxResults[0], nil +} + +// Simulate simulates the given txBytes to the network and returns the simulated response. +// TODO - this should be change to gRPC +func (n *UpgradeIntegrationNetwork) Simulate(txBytes []byte) (*txtypes.SimulateResponse, error) { + gas, result, err := n.app.BaseApp.Simulate(txBytes) + if err != nil { + return nil, err + } + return &txtypes.SimulateResponse{ + GasInfo: &gas, + Result: result, + }, nil +} + +// CheckTx calls the BaseApp's CheckTx method with the given txBytes to the network and returns the response. +func (n *UpgradeIntegrationNetwork) CheckTx(txBytes []byte) (*abcitypes.ResponseCheckTx, error) { + req := &abcitypes.RequestCheckTx{Tx: txBytes} + res, err := n.app.BaseApp.CheckTx(req) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/testutil/integration/exrp/upgrade/unit_network.go b/testutil/integration/exrp/upgrade/unit_network.go new file mode 100644 index 0000000..042a7a2 --- /dev/null +++ b/testutil/integration/exrp/upgrade/unit_network.go @@ -0,0 +1,57 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) +package exrpupgrade + +import ( + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/evmos/evmos/v20/x/evm/statedb" + inflationtypes "github.com/evmos/evmos/v20/x/inflation/v1/types" + "github.com/xrplevm/node/v6/app" + exrpcommon "github.com/xrplevm/node/v6/testutil/integration/exrp/common" +) + +// UnitTestUpgradeNetwork is the implementation of the Network interface for unit tests. +// It embeds the IntegrationNetwork struct to reuse its methods and +// makes the App public for easier testing. +type UnitTestUpgradeNetwork struct { + UpgradeIntegrationNetwork + App *app.App +} + +var _ Network = (*UnitTestUpgradeNetwork)(nil) + +// NewUnitTestNetwork configures and initializes a new Evmos Network instance with +// the given configuration options. If no configuration options are provided +// it uses the default configuration. +// +// It panics if an error occurs. +// Note: Only uses for Unit Tests +func NewUnitTestUpgradeNetwork(opts ...exrpcommon.ConfigOption) *UnitTestUpgradeNetwork { + network := New(opts...) + return &UnitTestUpgradeNetwork{ + UpgradeIntegrationNetwork: *network, + App: network.app, + } +} + +// GetStateDB returns the state database for the current block. +func (n *UnitTestUpgradeNetwork) GetStateDB() *statedb.StateDB { + headerHash := n.GetContext().HeaderHash() + return statedb.New( + n.GetContext(), + n.App.EvmKeeper, + statedb.NewEmptyTxConfig(common.BytesToHash(headerHash)), + ) +} + +// FundAccount funds the given account with the given amount of coins. +func (n *UnitTestUpgradeNetwork) FundAccount(addr sdktypes.AccAddress, coins sdktypes.Coins) error { + ctx := n.GetContext() + + if err := n.app.BankKeeper.MintCoins(ctx, inflationtypes.ModuleName, coins); err != nil { + return err + } + + return n.app.BankKeeper.SendCoinsFromModuleToAccount(ctx, inflationtypes.ModuleName, addr, coins) +} diff --git a/testutil/integration/exrp/utils/abci.go b/testutil/integration/exrp/utils/abci.go new file mode 100644 index 0000000..f0219ff --- /dev/null +++ b/testutil/integration/exrp/utils/abci.go @@ -0,0 +1,30 @@ +package utils + +import ( + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" +) + +type ValidatorFlagOverride struct { + Index int + Flag cmtproto.BlockIDFlag +} + +func NewValidatorFlagOverride(index int, flag cmtproto.BlockIDFlag) ValidatorFlagOverride { + return ValidatorFlagOverride{ + Index: index, + Flag: flag, + } +} + +func NewValidatorFlags(n int, overrides ...ValidatorFlagOverride) []cmtproto.BlockIDFlag { + flags := make([]cmtproto.BlockIDFlag, n) + for i := range flags { + flags[i] = cmtproto.BlockIDFlagCommit + } + + for _, override := range overrides { + flags[override.Index] = override.Flag + } + + return flags +} diff --git a/testutil/integration/exrp/utils/gov.go b/testutil/integration/exrp/utils/gov.go new file mode 100644 index 0000000..248ba28 --- /dev/null +++ b/testutil/integration/exrp/utils/gov.go @@ -0,0 +1,224 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package utils + +import ( + "errors" + "fmt" + "strconv" + + errorsmod "cosmossdk.io/errors" + "cosmossdk.io/math" + abcitypes "github.com/cometbft/cometbft/abci/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + commonfactory "github.com/xrplevm/node/v6/testutil/integration/common/factory" + "github.com/xrplevm/node/v6/testutil/integration/common/keyring" + "github.com/xrplevm/node/v6/testutil/integration/common/network" +) + +// SubmitProposal is a helper function to submit a governance proposal and +// return the proposal ID. +func SubmitProposal(tf commonfactory.BaseTxFactory, network network.Network, proposerPriv cryptotypes.PrivKey, title string, msgs ...sdk.Msg) (uint64, error) { + proposerAccAddr := sdk.AccAddress(proposerPriv.PubKey().Address()).String() + proposal, err := govv1.NewMsgSubmitProposal( + msgs, + sdk.NewCoins(sdk.NewCoin(network.GetDenom(), network.GetMinDepositAmt())), + proposerAccAddr, + "", + title, + title, + false, + ) + if err != nil { + return 0, err + } + + txArgs := commonfactory.CosmosTxArgs{ + Msgs: []sdk.Msg{proposal}, + } + + return submitProposal(tf, network, proposerPriv, txArgs) +} + +// SubmitLegacyProposal is a helper function to submit a governance proposal and +// return the proposal ID. +func SubmitLegacyProposal(tf commonfactory.BaseTxFactory, network network.Network, proposerPriv cryptotypes.PrivKey, proposal govv1beta1.Content) (uint64, error) { + proposerAccAddr := sdk.AccAddress(proposerPriv.PubKey().Address()) + + msgSubmitProposal, err := govv1beta1.NewMsgSubmitProposal( + proposal, + sdk.NewCoins(sdk.NewCoin(network.GetDenom(), math.NewInt(1e18))), + proposerAccAddr, + ) + if err != nil { + return 0, err + } + + txArgs := commonfactory.CosmosTxArgs{ + Msgs: []sdk.Msg{msgSubmitProposal}, + } + + return submitProposal(tf, network, proposerPriv, txArgs) +} + +// VoteOnProposal is a helper function to vote on a governance proposal given the private key of the voter and +// the option to vote. +func VoteOnProposal(tf commonfactory.BaseTxFactory, voterPriv cryptotypes.PrivKey, proposalID uint64, option govv1.VoteOption) (abcitypes.ExecTxResult, error) { + voterAccAddr := sdk.AccAddress(voterPriv.PubKey().Address()) + + msgVote := govv1.NewMsgVote( + voterAccAddr, + proposalID, + option, + "", + ) + + res, err := tf.CommitCosmosTx(voterPriv, commonfactory.CosmosTxArgs{ + Msgs: []sdk.Msg{msgVote}, + }) + + return res, err +} + +// ApproveProposal is a helper function to vote 'yes' +// for it and wait till it passes. +func ApproveProposal(tf commonfactory.BaseTxFactory, network network.Network, proposerPriv cryptotypes.PrivKey, proposalID uint64) error { + // Vote on proposal + if _, err := VoteOnProposal(tf, proposerPriv, proposalID, govv1.OptionYes); err != nil { + return errorsmod.Wrap(err, "failed to vote on proposal") + } + + if err := waitVotingPeriod(network); err != nil { + return errorsmod.Wrap(err, "failed to wait for voting period to pass") + } + + return checkProposalStatus(network, proposalID, govv1.StatusPassed) +} + +func SubmitAndAwaitProposalResolution(tf commonfactory.BaseTxFactory, network network.Network, keys []keyring.Key, title string, msgs ...sdk.Msg) (*govv1.Proposal, error) { + proposalID, err := SubmitProposal(tf, network, keys[0].Priv, title, msgs...) + if err != nil { + return nil, err + } + + for _, key := range keys { + _, err := VoteOnProposal(tf, key.Priv, proposalID, govv1.OptionYes) + if err != nil { + return nil, err + } + } + + if err := waitVotingPeriod(network); err != nil { + return nil, errorsmod.Wrap(err, "failed to wait for voting period to pass") + } + + return retrieveResolvedProposal(network, proposalID) +} + +func retrieveResolvedProposal(network network.Network, proposalID uint64) (*govv1.Proposal, error) { + gq := network.GetGovClient() + proposalRes, err := gq.Proposal(network.GetContext(), &govv1.QueryProposalRequest{ProposalId: proposalID}) + if err != nil { + return nil, err + } + return proposalRes.Proposal, nil +} + +// getProposalIDFromEvents returns the proposal ID from the events in +// the ResponseDeliverTx. +func getProposalIDFromEvents(events []abcitypes.Event) (uint64, error) { + var ( + err error + found bool + proposalID uint64 + ) + + for _, event := range events { + if event.Type != govtypes.EventTypeProposalDeposit { + continue + } + + for _, attr := range event.Attributes { + if attr.Key != govtypes.AttributeKeyProposalID { + continue + } + + proposalID, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return 0, errorsmod.Wrap(err, "failed to parse proposal ID") + } + + found = true + break + } + + if found { + break + } + } + + if !found { + return 0, errors.New("proposal deposit not found") + } + + return proposalID, nil +} + +func submitProposal(tf commonfactory.BaseTxFactory, network network.Network, proposerPriv cryptotypes.PrivKey, txArgs commonfactory.CosmosTxArgs) (uint64, error) { + res, err := tf.CommitCosmosTx(proposerPriv, txArgs) + if err != nil { + return 0, err + } + + proposalID, err := getProposalIDFromEvents(res.Events) + if err != nil { + return 0, errorsmod.Wrap(err, "failed to get proposal ID from events") + } + + err = network.NextBlock() + if err != nil { + return 0, errorsmod.Wrap(err, "failed to commit block after proposal") + } + + if err := checkProposalStatus(network, proposalID, govv1.StatusVotingPeriod); err != nil { + return 0, errorsmod.Wrap(err, "error while checking proposal") + } + + return proposalID, nil +} + +// waitVotingPeriod is a helper function that waits for the current voting period +// defined in the gov module params to pass +func waitVotingPeriod(n network.Network) error { + gq := n.GetGovClient() + params, err := gq.Params(n.GetContext(), &govv1.QueryParamsRequest{ParamsType: "voting"}) + if err != nil { + return errorsmod.Wrap(err, "failed to query voting params") + } + + err = n.NextBlockAfter(*params.Params.VotingPeriod) // commit after voting period is over + if err != nil { + return errorsmod.Wrap(err, "failed to commit block after voting period ends") + } + + return n.NextBlock() +} + +// checkProposalStatus is a helper function to check for a specific proposal status +func checkProposalStatus(n network.Network, proposalID uint64, expStatus govv1.ProposalStatus) error { + gq := n.GetGovClient() + proposalRes, err := gq.Proposal(n.GetContext(), &govv1.QueryProposalRequest{ProposalId: proposalID}) + if err != nil { + return errorsmod.Wrap(err, "failed to query proposal") + } + + if proposalRes.Proposal.Status != expStatus { + return fmt.Errorf("proposal status different than expected. Expected %s; got: %s", expStatus.String(), proposalRes.Proposal.Status.String()) + } + return nil +} diff --git a/x/poa/ante/poa_test.go b/x/poa/ante/poa_test.go index 2caf909..3d0acd1 100644 --- a/x/poa/ante/poa_test.go +++ b/x/poa/ante/poa_test.go @@ -1,6 +1,7 @@ package ante import ( + "errors" "testing" "time" @@ -9,6 +10,7 @@ import ( tmproto "github.com/cometbft/cometbft/proto/tendermint/types" sdktestutil "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" "github.com/xrplevm/node/v6/x/poa/testutil" @@ -16,7 +18,7 @@ import ( ) func setupPoaDecorator(t *testing.T) ( - *PoaDecorator, + PoaDecorator, sdk.Context, ) { key := storetypes.NewKVStoreKey(types.StoreKey) @@ -24,20 +26,50 @@ func setupPoaDecorator(t *testing.T) ( testCtx := sdktestutil.DefaultContextWithDB(t, key, tsKey) ctx := testCtx.Ctx.WithBlockHeader(tmproto.Header{Time: time.Now()}) - return &PoaDecorator{}, ctx + return NewPoaDecorator(), ctx } func TestPoaDecorator_AnteHandle(t *testing.T) { - pd, ctx := setupPoaDecorator(t) + tt := []struct { + name string + msgs []sdk.Msg + expectedError error + }{ + { + name: "should return error - tx not allowed", + msgs: []sdk.Msg{ + &stakingtypes.MsgUndelegate{}, + &stakingtypes.MsgBeginRedelegate{}, + &stakingtypes.MsgDelegate{}, + &stakingtypes.MsgCancelUnbondingDelegation{}, + }, + expectedError: errors.New("tx type not allowed"), + }, + { + name: "should not return error", + msgs: []sdk.Msg{ + &stakingtypes.MsgEditValidator{}, + }, + }, + } - ctrl := gomock.NewController(t) - txMock := testutil.NewMockTx(ctrl) - txMock.EXPECT().GetMsgs().Return([]sdk.Msg{}).AnyTimes() + for _, tc := range tt { + pd, ctx := setupPoaDecorator(t) - mockNext := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { - return ctx, nil - } + ctrl := gomock.NewController(t) + txMock := testutil.NewMockTx(ctrl) + txMock.EXPECT().GetMsgs().Return(tc.msgs).AnyTimes() - _, err := pd.AnteHandle(ctx, txMock, false, mockNext) - require.NoError(t, err) + mockNext := func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + return ctx, nil + } + + _, err := pd.AnteHandle(ctx, txMock, false, mockNext) + if tc.expectedError != nil { + require.Error(t, err) + require.Equal(t, tc.expectedError, err) + } else { + require.NoError(t, err) + } + } } diff --git a/x/poa/keeper/keeper.go b/x/poa/keeper/keeper.go index 77ba483..91f38de 100644 --- a/x/poa/keeper/keeper.go +++ b/x/poa/keeper/keeper.go @@ -20,6 +20,16 @@ import ( "github.com/xrplevm/node/v6/x/poa/types" ) +var _ types.QueryServer = Querier{} + +type Querier struct { + Keeper +} + +func NewQuerier(keeper Keeper) Querier { + return Querier{Keeper: keeper} +} + type ( Keeper struct { cdc codec.Codec diff --git a/x/poa/keeper/keeper_test.go b/x/poa/keeper/keeper_test.go index 3173524..4858da6 100644 --- a/x/poa/keeper/keeper_test.go +++ b/x/poa/keeper/keeper_test.go @@ -1,6 +1,7 @@ package keeper import ( + "errors" "testing" "cosmossdk.io/math" @@ -50,32 +51,621 @@ func poaKeeperTestSetup(t *testing.T) (*Keeper, sdk.Context) { } // Define here Keeper methods to be unit tested -func TestPoAKeeper_ExecuteAddValidator(t *testing.T) { - keeper, ctx := poaKeeperTestSetup(t) +func TestKeeper_ExecuteAddValidator(t *testing.T) { ctrl := gomock.NewController(t) pubKey := testutil.NewMockPubKey(ctrl) msgPubKey, _ := types1.NewAnyWithValue(pubKey) - msg := &types.MsgAddValidator{ - Authority: keeper.GetAuthority(), - ValidatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", - Description: stakingtypes.Description{ - Moniker: "test", - Identity: "test", - Website: "test", - SecurityContact: "test", - Details: "test", - }, - Pubkey: msgPubKey, + tt := []struct { + name string + validatorAddress string + pubKey *types1.Any + stakingMocks func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) + bankMocks func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) + expectedError error + }{ + { + name: "should fail - invalid validator address", + validatorAddress: "invalidnaddress", + expectedError: errors.New("decoding bech32 failed"), + stakingMocks: func(_ sdk.Context, _ *testutil.MockStakingKeeper) {}, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + { + name: "should fail - staking keeper returns error on GetParams", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: errors.New("staking params error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{}, errors.New("staking params error")) + }, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + { + name: "should fail - maximum validators reached", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: types.ErrMaxValidatorsReached, + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 1, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{{}}, nil) + }, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + { + name: "should fail - validator has bonded tokens", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: types.ErrAddressHasBankTokens, + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: sdk.DefaultPowerReduction, + }) + }, + }, + { + name: "should fail - staking keeper returns error on GetValidator", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: errors.New("staking validator error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{}, errors.New("staking validator error")) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + }, + }, + { + name: "should fail - staking keeper returns validator with tokens", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: types.ErrAddressHasBondedTokens, + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: sdk.DefaultPowerReduction}, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + }, + }, + { + name: "should fail - staking keeper returns error on GetAllDelegatorDelegations", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: errors.New("staking delegations error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{}, errors.New("staking delegations error")) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + }, + }, + { + name: "should fail - delegations are greater than 0 with invalid delegation validator address", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: errors.New("decoding bech32 failed"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{ + { + ValidatorAddress: "invalidvalidatoraddress", + Shares: sdk.DefaultPowerReduction.ToLegacyDec(), + }, + }, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + }, + }, + { + name: "should fail - delegations are greater than 0 with error on GetValidator call", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: errors.New("staking validator error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil).Times(1) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, errors.New("staking validator error")).Times(1) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{ + { + ValidatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + DelegatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + Shares: sdk.DefaultPowerReduction.ToLegacyDec(), + }, + }, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + }, + }, + { + name: "should fail - delegations are greater than 0 with delegated tokens", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: types.ErrAddressHasDelegatedTokens, + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil).Times(1) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: sdk.DefaultPowerReduction}, nil).Times(1) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{ + { + ValidatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + DelegatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + Shares: sdk.DefaultPowerReduction.ToLegacyDec(), + }, + }, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + }, + }, + { + name: "should fail - GetUnbondingDelegationsFromValidator returns error", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: errors.New("staking unbonding delegations error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{}, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return([]stakingtypes.UnbondingDelegation{}, errors.New("staking unbonding delegations error")) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + }, + }, + { + name: "should fail - unbonding delegations balances are greater than 0", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: types.ErrAddressHasUnbondingTokens, + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{}, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return([]stakingtypes.UnbondingDelegation{ + { + Entries: []stakingtypes.UnbondingDelegationEntry{ + { + Balance: sdk.DefaultPowerReduction, + }, + }, + }, + }, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + }, + }, + { + name: "should fail - bank keeper MintCoins returns error", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: errors.New("bank mint coins error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{}, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return([]stakingtypes.UnbondingDelegation{}, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + bankKeeper.EXPECT().MintCoins(ctx, gomock.Any(), gomock.Any()).Return(errors.New("bank mint coins error")) + }, + }, + { + name: "should fail - bank keeper SendCoinsFromModuleToAccount returns error", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedError: errors.New("bank send coins from module to account error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{}, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return([]stakingtypes.UnbondingDelegation{}, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + bankKeeper.EXPECT().MintCoins(ctx, gomock.Any(), gomock.Any()).Return(nil) + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("bank send coins from module to account error")) + }, + }, + { + name: "should pass - MsgAddValidator", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + pubKey: msgPubKey, + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{}, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return([]stakingtypes.UnbondingDelegation{}, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + bankKeeper.EXPECT().MintCoins(ctx, gomock.Any(), gomock.Any()).Return(nil) + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + }, + }, + { + name: "should pass - validator not found when iterating over delegator delegations", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + pubKey: msgPubKey, + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + MaxValidators: 2, + }, nil) + stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil).Times(1) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, stakingtypes.ErrNoValidatorFound).Times(1) + stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{ + { + ValidatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + DelegatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + Shares: sdk.DefaultPowerReduction.ToLegacyDec(), + }, + }, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return([]stakingtypes.UnbondingDelegation{}, nil) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{ + Denom: "BND", + Amount: math.NewInt(0), + }) + bankKeeper.EXPECT().MintCoins(ctx, gomock.Any(), gomock.Any()).Return(nil) + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + }, + }, } - err := keeper.ExecuteAddValidator(ctx, msg) - require.NoError(t, err) + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + keeper, ctx := setupPoaKeeper(t, tc.stakingMocks, tc.bankMocks) + + msg := &types.MsgAddValidator{ + Authority: keeper.GetAuthority(), + ValidatorAddress: tc.validatorAddress, + Description: stakingtypes.Description{ + Moniker: "test", + Identity: "test", + Website: "test", + SecurityContact: "test", + Details: "test", + }, + Pubkey: tc.pubKey, + } + + err := keeper.ExecuteAddValidator(ctx, msg) + if tc.expectedError != nil { + require.Contains(t, err.Error(), tc.expectedError.Error()) + } else { + require.NoError(t, err) + } + }) + } } -func TestPoAKeeper_ExecuteRemoveValidator(t *testing.T) { - keeper, ctx := poaKeeperTestSetup(t) +func TestKeeper_ExecuteRemoveValidator(t *testing.T) { + ctrl := gomock.NewController(t) - err := keeper.ExecuteRemoveValidator(ctx, "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu") - require.NoError(t, err) + tt := []struct { + name string + validatorAddress string + stakingMocks func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) + bankMocks func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) + expectedError error + }{ + { + name: "should fail - invalid validator address", + validatorAddress: "invalidnaddress", + expectedError: errors.New("decoding bech32 failed"), + stakingMocks: func(_ sdk.Context, _ *testutil.MockStakingKeeper) {}, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + { + name: "should fail - staking keeper returns error on GetParams", + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + expectedError: errors.New("staking params error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{}, errors.New("staking params error")) + }, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + { + name: "should fail - staking keeper returns error on GetValidator", + expectedError: types.ErrAddressIsNotAValidator, + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + }, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{}, errors.New("staking keeper get validator error")) + }, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + { + name: "should fail - staking keeper returns error on call GetUnbondingDelegationsFromValidator", + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + expectedError: errors.New("staking keeper get unbonding delegations from validator error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + }, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return( + []stakingtypes.UnbondingDelegation{}, errors.New("staking keeper get unbonding delegations from validator error")) + hooks := testutil.NewMockStakingHooks(ctrl) + hooks.EXPECT().BeforeValidatorModified(ctx, gomock.Any()).Return(errors.New("staking keeper hooks error")) + stakingKeeper.EXPECT().Hooks().Return(hooks) + }, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + { + name: "should fail - staking keeper returns error on call SlashUnbondingDelegation", + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + expectedError: errors.New("staking keeper slash unbonding delegation error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + }, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{}, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return( + []stakingtypes.UnbondingDelegation{ + { + ValidatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + }, + }, nil) + + hooks := testutil.NewMockStakingHooks(ctrl) + hooks.EXPECT().BeforeValidatorModified(ctx, gomock.Any()).Return(nil) + stakingKeeper.EXPECT().Hooks().Return(hooks) + + stakingKeeper.EXPECT().SlashUnbondingDelegation(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return( + math.NewInt(0), errors.New("staking keeper slash unbonding delegation error")) + }, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + { + name: "should fail - staking keeper returns error on RemoveValidatorTokens call", + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + expectedError: errors.New("staking keeper remove validator tokens error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + }, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{ + Tokens: sdk.DefaultPowerReduction, + }, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return( + []stakingtypes.UnbondingDelegation{}, nil, + ) + + hooks := testutil.NewMockStakingHooks(ctrl) + hooks.EXPECT().BeforeValidatorModified(ctx, gomock.Any()).Return(nil) + hooks.EXPECT().BeforeValidatorSlashed(ctx, gomock.Any(), gomock.Any()).Return(errors.New("staking keeper hook error")) + stakingKeeper.EXPECT().Hooks().Return(hooks).AnyTimes() + + stakingKeeper.EXPECT().RemoveValidatorTokens(ctx, gomock.Any(), gomock.Any()).Return( + stakingtypes.Validator{}, errors.New("staking keeper remove validator tokens error"), + ) + }, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + //nolint:dupl + { + name: "should fail - bank keeper returns error on call BurnCoins for status bonded", + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + expectedError: errors.New("bank keeper burn coins error"), + //nolint:dupl + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + }, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{ + Tokens: math.NewInt(0), + }, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return( + []stakingtypes.UnbondingDelegation{}, nil, + ) + + hooks := testutil.NewMockStakingHooks(ctrl) + hooks.EXPECT().BeforeValidatorModified(ctx, gomock.Any()).Return(nil) + stakingKeeper.EXPECT().Hooks().Return(hooks).AnyTimes() + + stakingKeeper.EXPECT().RemoveValidatorTokens(ctx, gomock.Any(), gomock.Any()).Return( + stakingtypes.Validator{ + Status: stakingtypes.Bonded, + }, nil, + ) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().BurnCoins(ctx, gomock.Any(), gomock.Any()).Return(errors.New("bank keeper burn coins error")) + }, + }, + //nolint:dupl + { + name: "should fail - bank keeper returns error on call BurnCoins for status unbonding/unbonded", + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + expectedError: errors.New("bank keeper burn coins error"), + //nolint:dupl + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + }, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{ + Tokens: math.NewInt(0), + }, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return( + []stakingtypes.UnbondingDelegation{}, nil, + ) + + hooks := testutil.NewMockStakingHooks(ctrl) + hooks.EXPECT().BeforeValidatorModified(ctx, gomock.Any()).Return(nil) + stakingKeeper.EXPECT().Hooks().Return(hooks).AnyTimes() + + stakingKeeper.EXPECT().RemoveValidatorTokens(ctx, gomock.Any(), gomock.Any()).Return( + stakingtypes.Validator{ + Status: stakingtypes.Unbonding, + }, nil, + ) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().BurnCoins(ctx, gomock.Any(), gomock.Any()).Return(errors.New("bank keeper burn coins error")) + }, + }, + { + name: "should fail - bank keeper returns error for invalid validator status", + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + expectedError: types.ErrInvalidValidatorStatus, + //nolint:dupl + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + }, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{ + Tokens: math.NewInt(0), + }, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return( + []stakingtypes.UnbondingDelegation{}, nil, + ) + + hooks := testutil.NewMockStakingHooks(ctrl) + hooks.EXPECT().BeforeValidatorModified(ctx, gomock.Any()).Return(nil) + stakingKeeper.EXPECT().Hooks().Return(hooks).AnyTimes() + + stakingKeeper.EXPECT().RemoveValidatorTokens(ctx, gomock.Any(), gomock.Any()).Return( + stakingtypes.Validator{ + Status: stakingtypes.Unspecified, + }, nil, + ) + }, + bankMocks: func(_ sdk.Context, _ *testutil.MockBankKeeper) {}, + }, + { + name: "should fail - staking keeper returns error on call Unbond", + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + expectedError: errors.New("staking keeper unbond error"), + stakingMocks: func(ctx sdk.Context, stakingKeeper *testutil.MockStakingKeeper) { + stakingKeeper.EXPECT().GetParams(ctx).Return(stakingtypes.Params{ + BondDenom: "BND", + }, nil) + stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{ + Tokens: math.NewInt(0), + }, nil) + stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return( + []stakingtypes.UnbondingDelegation{}, nil, + ) + + hooks := testutil.NewMockStakingHooks(ctrl) + hooks.EXPECT().BeforeValidatorModified(ctx, gomock.Any()).Return(nil) + stakingKeeper.EXPECT().Hooks().Return(hooks).AnyTimes() + + stakingKeeper.EXPECT().RemoveValidatorTokens(ctx, gomock.Any(), gomock.Any()).Return( + stakingtypes.Validator{ + Status: stakingtypes.Bonded, + }, nil, + ) + + stakingKeeper.EXPECT().Unbond(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return( + math.NewInt(0), errors.New("staking keeper unbond error"), + ) + }, + bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) { + bankKeeper.EXPECT().BurnCoins(ctx, gomock.Any(), gomock.Any()).Return(nil) + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + keeper, ctx := setupPoaKeeper(t, tc.stakingMocks, tc.bankMocks) + + err := keeper.ExecuteRemoveValidator(ctx, tc.validatorAddress) + if tc.expectedError != nil { + require.Contains(t, err.Error(), tc.expectedError.Error()) + } else { + require.NoError(t, err) + } + }) + } } diff --git a/x/poa/keeper/msg_server_add_validator_test.go b/x/poa/keeper/msg_server_add_validator_test.go index 373d85a..00e0b0b 100644 --- a/x/poa/keeper/msg_server_add_validator_test.go +++ b/x/poa/keeper/msg_server_add_validator_test.go @@ -1,9 +1,11 @@ package keeper import ( + "errors" "testing" types1 "github.com/cosmos/cosmos-sdk/codec/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" @@ -19,19 +21,53 @@ func TestMsgServer_AddValidator(t *testing.T) { msgPubKey, _ := types1.NewAnyWithValue(pubKey) msgServer := NewMsgServerImpl(*poaKeeper) - msg := &types.MsgAddValidator{ - Authority: poaKeeper.GetAuthority(), - ValidatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", - Description: stakingtypes.Description{ - Moniker: "test", - Identity: "test", - Website: "test", - SecurityContact: "test", - Details: "test", + tt := []struct { + name string + authority string + validatorAddress string + expectedErr error + }{ + { + name: "should fail - invalid authority address", + authority: "invalidauthority", + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", + expectedErr: govtypes.ErrInvalidSigner, + }, + { + name: "should fail - invalid validator address", + authority: poaKeeper.GetAuthority(), + validatorAddress: "invalidvalidatoraddress", + expectedErr: errors.New("decoding bech32 failed"), + }, + { + name: "should pass", + authority: poaKeeper.GetAuthority(), + validatorAddress: "ethm1a0pd5cyew47pvgf7rd7axxy3humv9ev0nnkprp", }, - Pubkey: msgPubKey, } - _, err := msgServer.AddValidator(ctx, msg) - require.NoError(t, err) + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + msg := &types.MsgAddValidator{ + Authority: tc.authority, + ValidatorAddress: tc.validatorAddress, + Description: stakingtypes.Description{ + Moniker: "test", + Identity: "test", + Website: "test", + SecurityContact: "test", + Details: "test", + }, + Pubkey: msgPubKey, + } + + _, err := msgServer.AddValidator(ctx, msg) + if tc.expectedErr != nil { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr.Error()) + } else { + require.NoError(t, err) + } + }) + } } diff --git a/x/poa/keeper/msg_server_remove_validator_test.go b/x/poa/keeper/msg_server_remove_validator_test.go index 862e371..32d0b5b 100644 --- a/x/poa/keeper/msg_server_remove_validator_test.go +++ b/x/poa/keeper/msg_server_remove_validator_test.go @@ -1,8 +1,10 @@ package keeper import ( + "errors" "testing" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/stretchr/testify/require" "github.com/xrplevm/node/v6/x/poa/types" ) @@ -12,11 +14,45 @@ func TestMsgServer_RemoveValidator(t *testing.T) { msgServer := NewMsgServerImpl(*poaKeeper) - msg := &types.MsgRemoveValidator{ - Authority: poaKeeper.GetAuthority(), - ValidatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + tt := []struct { + name string + authority string + validatorAddress string + expectedErr error + }{ + { + name: "should fail - invalid authority address", + authority: "invalidauthority", + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + expectedErr: govtypes.ErrInvalidSigner, + }, + { + name: "should fail - invalid validator address", + authority: poaKeeper.GetAuthority(), + validatorAddress: "invalidvalidatoraddress", + expectedErr: errors.New("decoding bech32 failed"), + }, + { + name: "should pass", + authority: poaKeeper.GetAuthority(), + validatorAddress: "ethmvaloper1a0pd5cyew47pvgf7rd7axxy3humv9ev0urudmu", + }, } - _, err := msgServer.RemoveValidator(ctx, msg) - require.NoError(t, err) + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + msg := &types.MsgRemoveValidator{ + Authority: tc.authority, + ValidatorAddress: tc.validatorAddress, + } + + _, err := msgServer.RemoveValidator(ctx, msg) + if tc.expectedErr != nil { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr.Error()) + } else { + require.NoError(t, err) + } + }) + } }