diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f24791e18e..442c2f07ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -117,4 +117,84 @@ jobs: - name: Test e2e run: | make test-integration + if: env.GIT_DIFF + + test-sim-nondeterminism: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.17 + check-latest: true + - uses: actions/checkout@v3 + - uses: technote-space/get-diff-action@v6.0.1 + with: + PATTERNS: | + **/**.go + go.mod + go.sum + - name: Test x/evm simulation nondeterminism + run: | + make test-sim-nondeterminism + if: env.GIT_DIFF + + test-sim-random-genesis-fast: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.17 + check-latest: true + - uses: actions/checkout@v3 + - uses: technote-space/get-diff-action@v6.0.1 + with: + PATTERNS: | + **/**.go + go.mod + go.sum + - name: Test x/evm simulation with random genesis + run: | + make test-sim-random-genesis-fast + if: env.GIT_DIFF + + test-sim-import-export: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.17 + check-latest: true + - uses: actions/checkout@v3 + - uses: technote-space/get-diff-action@v6.0.1 + with: + PATTERNS: | + **/**.go + go.mod + go.sum + - name: Test x/evm simulation import and export + run: | + make test-sim-import-export + if: env.GIT_DIFF + + test-sim-after-import: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.17 + check-latest: true + - uses: actions/checkout@v3 + - uses: technote-space/get-diff-action@v6.0.1 + with: + PATTERNS: | + **/**.go + go.mod + go.sum + - name: Test x/evm simulation after import + run: | + make test-sim-after-import if: env.GIT_DIFF \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f306402f..81f2bf0794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ * Move `rpc/ethereum/backend` -> `rpc/backend` * Move `rpc/ethereum/namespaces` -> `rpc/namespaces/ethereum` +### Improvements + +* (ci, evm) [tharsis#1063](https://github.com/tharsis/ethermint/pull/1063) Run simulations on CI. + ### Bug Fixes * (rpc) [tharsis#1059](https://github.com/tharsis/ethermint/pull/1059) Remove unnecessary event filtering logic on the `eth_baseFee` JSON-RPC endpoint. diff --git a/Makefile b/Makefile index 8d79416838..25b8f92cc2 100755 --- a/Makefile +++ b/Makefile @@ -201,7 +201,7 @@ RUNSIM = $(TOOLS_DESTDIR)/runsim runsim: $(RUNSIM) $(RUNSIM): @echo "Installing runsim..." - @(cd /tmp && ${GO_MOD} go get github.com/cosmos/tools/cmd/runsim@master) + @(cd /tmp && ${GO_MOD} go install github.com/cosmos/tools/cmd/runsim@master) statik: $(STATIK) $(STATIK): @@ -343,7 +343,7 @@ test-sim-nondeterminism: -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h test-sim-random-genesis-fast: - @echo "Running custom genesis simulation..." + @echo "Running random genesis simulation..." @go test -mod=readonly $(SIMAPP) -run TestFullAppSimulation \ -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -v -timeout 24h diff --git a/app/utils.go b/app/utils.go index b9f3831224..95fb576fc9 100644 --- a/app/utils.go +++ b/app/utils.go @@ -108,7 +108,7 @@ func RandomAccounts(r *rand.Rand, n int) []simtypes.Account { prv := secp256k1.GenPrivKeyFromSecret(privkeySeed) ethPrv := ðsecp256k1.PrivKey{} - _ = ethPrv.UnmarshalAmino(prv.Bytes()) + _ = ethPrv.UnmarshalAmino(prv.Bytes()) // UnmarshalAmino simply copies the bytes and assigns them to ethPrv.Key accs[i].PrivKey = ethPrv accs[i].PubKey = accs[i].PrivKey.PubKey() accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address()) diff --git a/app/utils_test.go b/app/utils_test.go index 1ad1b16e49..6029e059af 100644 --- a/app/utils_test.go +++ b/app/utils_test.go @@ -1,7 +1,9 @@ package app import ( + "encoding/json" "math/rand" + "os" "testing" "github.com/stretchr/testify/require" @@ -11,7 +13,10 @@ import ( authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + evmtypes "github.com/tharsis/ethermint/x/evm/types" + "github.com/tharsis/ethermint/crypto/ethsecp256k1" ethermint "github.com/tharsis/ethermint/types" "github.com/cosmos/cosmos-sdk/simapp" @@ -53,3 +58,54 @@ func TestRandomGenesisAccounts(t *testing.T) { require.True(t, ok) } } + +func TestStateFn(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping AppStateFn testing") + } + require.NoError(t, err, "simulation setup failed") + + config.ChainID = SimAppChainID + config.Commit = true + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewEthermintApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), simapp.EmptyAppOptions{}, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + appStateFn := StateFn(app.AppCodec(), app.SimulationManager()) + r := rand.New(rand.NewSource(seed)) + accounts := RandomAccounts(r, rand.Intn(maxTestingAccounts)) + appState, _, _, _ := appStateFn(r, accounts, config) + + rawState := make(map[string]json.RawMessage) + err = json.Unmarshal(appState, &rawState) + require.NoError(t, err) + + stakingStateBz, ok := rawState[stakingtypes.ModuleName] + require.True(t, ok) + + stakingState := new(stakingtypes.GenesisState) + app.AppCodec().MustUnmarshalJSON(stakingStateBz, stakingState) + bondDenom := stakingState.Params.BondDenom + + evmStateBz, ok := rawState[evmtypes.ModuleName] + require.True(t, ok) + + evmState := new(evmtypes.GenesisState) + app.AppCodec().MustUnmarshalJSON(evmStateBz, evmState) + require.Equal(t, bondDenom, evmState.Params.EvmDenom) +} + +func TestRandomAccounts(t *testing.T) { + r := rand.New(rand.NewSource(seed)) + accounts := RandomAccounts(r, rand.Intn(maxTestingAccounts)) + for _, acc := range accounts { + _, ok := acc.PrivKey.(*ethsecp256k1.PrivKey) + require.True(t, ok) + } +} diff --git a/x/evm/simulation/decoder.go b/x/evm/simulation/decoder.go index e09e870c02..5f646851b0 100644 --- a/x/evm/simulation/decoder.go +++ b/x/evm/simulation/decoder.go @@ -10,18 +10,18 @@ import ( ) // NewDecodeStore returns a decoder function closure that unmarshals the KVPair's -// Value to the corresponding evm type. +// value to the corresponding EVM type. func NewDecodeStore() func(kvA, kvB kv.Pair) string { return func(kvA, kvB kv.Pair) string { switch { case bytes.Equal(kvA.Key[:1], types.KeyPrefixStorage): - storageHashA := common.BytesToHash(kvA.Value).Hex() - storageHashB := common.BytesToHash(kvB.Value).Hex() + storageA := common.BytesToHash(kvA.Value).Hex() + storageB := common.BytesToHash(kvB.Value).Hex() - return fmt.Sprintf("%v\n%v", storageHashA, storageHashB) + return fmt.Sprintf("%v\n%v", storageA, storageB) case bytes.Equal(kvA.Key[:1], types.KeyPrefixCode): - codeHashA := common.BytesToHash(kvA.Value).Hex() - codeHashB := common.BytesToHash(kvB.Value).Hex() + codeHashA := common.Bytes2Hex(kvA.Value) + codeHashB := common.Bytes2Hex(kvB.Value) return fmt.Sprintf("%v\n%v", codeHashA, codeHashB) default: diff --git a/x/evm/simulation/decoder_test.go b/x/evm/simulation/decoder_test.go new file mode 100644 index 0000000000..2dbd85c66b --- /dev/null +++ b/x/evm/simulation/decoder_test.go @@ -0,0 +1,47 @@ +package simulation + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/types/kv" + "github.com/ethereum/go-ethereum/common" + "github.com/tharsis/ethermint/x/evm/types" +) + +// TestDecodeStore tests that evm simulation decoder decodes the key value pairs as expected. +func TestDecodeStore(t *testing.T) { + dec := NewDecodeStore() + + hash := common.BytesToHash([]byte("hash")) + code := common.Bytes2Hex([]byte{1, 2, 3}) + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.KeyPrefixCode, Value: common.FromHex(code)}, + {Key: types.KeyPrefixStorage, Value: hash.Bytes()}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"Code", fmt.Sprintf("%v\n%v", code, code)}, + {"Storage", fmt.Sprintf("%v\n%v", hash, hash)}, + {"other", ""}, + } + for i, tt := range tests { + i, tt := i, tt + t.Run(tt.name, func(t *testing.T) { + switch i { + case len(tests) - 1: + require.Panics(t, func() { dec(kvPairs.Pairs[i], kvPairs.Pairs[i]) }, tt.name) + default: + require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i]), tt.name) + } + }) + } +} diff --git a/x/evm/simulation/genesis.go b/x/evm/simulation/genesis.go index 17f65cdb0c..73110b89d7 100644 --- a/x/evm/simulation/genesis.go +++ b/x/evm/simulation/genesis.go @@ -10,18 +10,44 @@ import ( "github.com/tharsis/ethermint/x/evm/types" ) -// GenExtraEIPs randomly generates specific extra eips or not. -func genExtraEIPs(r *rand.Rand) []int64 { +const ( + extraEIPsKey = "extra_eips" +) + +// GenExtraEIPs defines a set of extra EIPs with 50% probability +func GenExtraEIPs(r *rand.Rand) []int64 { var extraEIPs []int64 - if r.Uint32()%2 == 0 { + // 50% chance of having extra EIPs + if r.Intn(2) == 0 { extraEIPs = []int64{1344, 1884, 2200, 2929, 3198, 3529} } return extraEIPs } -// RandomizedGenState generates a random GenesisState for nft +// GenEnableCreate enables the EnableCreate param with 80% probability +func GenEnableCreate(r *rand.Rand) bool { + // 80% chance of enabling create contract + enableCreate := r.Intn(100) < 80 + return enableCreate +} + +// GenEnableCall enables the EnableCall param with 80% probability +func GenEnableCall(r *rand.Rand) bool { + // 80% chance of enabling evm account transfer and calling contract + enableCall := r.Intn(100) < 80 + return enableCall +} + +// RandomizedGenState generates a random GenesisState for the EVM module func RandomizedGenState(simState *module.SimulationState) { - extraEIPs := genExtraEIPs(simState.Rand) + // evm params + var extraEIPs []int64 + + simState.AppParams.GetOrGenerate( + simState.Cdc, extraEIPsKey, &extraEIPs, simState.Rand, + func(r *rand.Rand) { extraEIPs = GenExtraEIPs(r) }, + ) + params := types.NewParams(types.DefaultEVMDenom, true, true, types.DefaultChainConfig(), extraEIPs...) evmGenesis := types.NewGenesisState(params, []types.GenesisAccount{}) diff --git a/x/evm/simulation/genesis_test.go b/x/evm/simulation/genesis_test.go new file mode 100644 index 0000000000..b45e35396d --- /dev/null +++ b/x/evm/simulation/genesis_test.go @@ -0,0 +1,50 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/tharsis/ethermint/x/evm/simulation" + "github.com/tharsis/ethermint/x/evm/types" +) + +// TestRandomizedGenState tests the normal scenario of applying RandomizedGenState. +// Abonormal scenarios are not tested here. +func TestRandomizedGenState(t *testing.T) { + registry := codectypes.NewInterfaceRegistry() + types.RegisterInterfaces(registry) + cdc := codec.NewProtoCodec(registry) + + s := rand.NewSource(1) + r := rand.New(s) + + simState := module.SimulationState{ + AppParams: make(simtypes.AppParams), + Cdc: cdc, + Rand: r, + NumBonded: 3, + Accounts: simtypes.RandomAccounts(r, 3), + InitialStake: 1000, + GenState: make(map[string]json.RawMessage), + } + + simulation.RandomizedGenState(&simState) + + var evmGenesis types.GenesisState + simState.Cdc.MustUnmarshalJSON(simState.GenState[types.ModuleName], &evmGenesis) + + require.Equal(t, true, evmGenesis.Params.GetEnableCreate()) + require.Equal(t, true, evmGenesis.Params.GetEnableCall()) + require.Equal(t, types.DefaultEVMDenom, evmGenesis.Params.GetEvmDenom()) + require.Equal(t, simulation.GenExtraEIPs(r), evmGenesis.Params.GetExtraEIPs()) + require.Equal(t, types.DefaultChainConfig(), evmGenesis.Params.GetChainConfig()) + + require.Equal(t, len(evmGenesis.Accounts), 0) +} diff --git a/x/evm/simulation/operations.go b/x/evm/simulation/operations.go index 942cbf2da4..d0ad101c70 100644 --- a/x/evm/simulation/operations.go +++ b/x/evm/simulation/operations.go @@ -25,7 +25,6 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth/signing" "github.com/ethereum/go-ethereum/crypto" "github.com/tharsis/ethermint/encoding" - "github.com/tharsis/ethermint/server/config" "github.com/tharsis/ethermint/tests" "github.com/tharsis/ethermint/x/evm/keeper" "github.com/tharsis/ethermint/x/evm/types" @@ -203,10 +202,12 @@ func SimulateEthTx( // CreateRandomValidEthTx create the ethereum tx with valid random values func CreateRandomValidEthTx(ctx *simulateContext, from, to *common.Address, amount *big.Int, data *hexutil.Bytes) (ethTx *types.MsgEthereumTx, err error) { - estimateGas, err := EstimateGas(ctx, from, to, data) + gasCap := ctx.rand.Uint64() + estimateGas, err := EstimateGas(ctx, from, to, data, gasCap) if err != nil { return nil, err } + // we suppose that gasLimit should be larger than estimateGas to ensure tx validity gasLimit := estimateGas + uint64(ctx.rand.Intn(int(sdktx.MaxGasWanted-estimateGas))) ethChainID := ctx.keeper.ChainID() chainConfig := ctx.keeper.GetParams(ctx.context).ChainConfig.EthereumConfig(ethChainID) @@ -216,7 +217,7 @@ func CreateRandomValidEthTx(ctx *simulateContext, from, to *common.Address, amou nonce := ctx.keeper.GetNonce(ctx.context, *from) if amount == nil { - amount, err = RandomTransferableAmount(ctx, *from, gasLimit, gasFeeCap) + amount, err = RandomTransferableAmount(ctx, *from, estimateGas, gasFeeCap) if err != nil { return nil, err } @@ -228,7 +229,7 @@ func CreateRandomValidEthTx(ctx *simulateContext, from, to *common.Address, amou } // EstimateGas estimates the gas used by quering the keeper. -func EstimateGas(ctx *simulateContext, from, to *common.Address, data *hexutil.Bytes) (gas uint64, err error) { +func EstimateGas(ctx *simulateContext, from, to *common.Address, data *hexutil.Bytes, gasCap uint64) (gas uint64, err error) { args, err := json.Marshal(&types.TransactionArgs{To: to, From: from, Data: data}) if err != nil { return 0, err @@ -236,7 +237,7 @@ func EstimateGas(ctx *simulateContext, from, to *common.Address, data *hexutil.B res, err := ctx.keeper.EstimateGas(sdk.WrapSDKContext(ctx.context), &types.EthCallRequest{ Args: args, - GasCap: config.DefaultGasCap, + GasCap: gasCap, }) if err != nil { return 0, err @@ -246,9 +247,9 @@ func EstimateGas(ctx *simulateContext, from, to *common.Address, data *hexutil.B // RandomTransferableAmount generates a random valid transferable amount. // Transferable amount is between the range [0, spendable), spendable = balance - gasFeeCap * GasLimit. -func RandomTransferableAmount(ctx *simulateContext, address common.Address, gasLimit uint64, gasFeeCap *big.Int) (amount *big.Int, err error) { +func RandomTransferableAmount(ctx *simulateContext, address common.Address, estimateGas uint64, gasFeeCap *big.Int) (amount *big.Int, err error) { balance := ctx.keeper.GetBalance(ctx.context, address) - feeLimit := new(big.Int).Mul(gasFeeCap, big.NewInt(int64(gasLimit))) + feeLimit := new(big.Int).Mul(gasFeeCap, big.NewInt(int64(estimateGas))) if (feeLimit.Cmp(balance)) > 0 { return nil, ErrNoEnoughBalance } diff --git a/x/evm/simulation/params.go b/x/evm/simulation/params.go index 94295a5cd2..e7b9914374 100644 --- a/x/evm/simulation/params.go +++ b/x/evm/simulation/params.go @@ -6,22 +6,35 @@ import ( "fmt" "math/rand" + amino "github.com/cosmos/cosmos-sdk/codec" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/tharsis/ethermint/x/evm/types" ) -const ( - keyExtraEIPs = "ExtraEIPs" -) - // ParamChanges defines the parameters that can be modified by param change proposals // on the simulation. func ParamChanges(r *rand.Rand) []simtypes.ParamChange { return []simtypes.ParamChange{ - simulation.NewSimParamChange(types.ModuleName, keyExtraEIPs, + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyExtraEIPs), + func(r *rand.Rand) string { + extraEIPs := GenExtraEIPs(r) + amino := amino.NewLegacyAmino() + bz, err := amino.MarshalJSON(extraEIPs) + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyEnableCreate), + func(r *rand.Rand) string { + return fmt.Sprintf("%v", GenEnableCreate(r)) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.ParamStoreKeyEnableCall), func(r *rand.Rand) string { - return fmt.Sprintf("\"%d\"", genExtraEIPs(r)) + return fmt.Sprintf("%v", GenEnableCall(r)) }, ), } diff --git a/x/evm/simulation/params_test.go b/x/evm/simulation/params_test.go new file mode 100644 index 0000000000..a67d0c9d21 --- /dev/null +++ b/x/evm/simulation/params_test.go @@ -0,0 +1,44 @@ +package simulation_test + +import ( + "encoding/json" + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tharsis/ethermint/x/evm/simulation" +) + +// TestParamChanges tests the paramChanges are generated as expected. +func TestParamChanges(t *testing.T) { + s := rand.NewSource(1) + r := rand.New(s) + + extraEIPs := simulation.GenExtraEIPs(r) + bz, err := json.Marshal(extraEIPs) + require.NoError(t, err) + + expected := []struct { + composedKey string + key string + simValue string + subspace string + }{ + {"evm/EnableExtraEIPs", "EnableExtraEIPs", string(bz), "evm"}, + {"evm/EnableCreate", "EnableCreate", fmt.Sprintf("%v", simulation.GenEnableCreate(r)), "evm"}, + {"evm/EnableCall", "EnableCall", fmt.Sprintf("%v", simulation.GenEnableCall(r)), "evm"}, + } + + paramChanges := simulation.ParamChanges(r) + + require.Len(t, paramChanges, 3) + + for i, p := range paramChanges { + require.Equal(t, expected[i].composedKey, p.ComposedKey()) + require.Equal(t, expected[i].key, p.Key()) + require.Equal(t, expected[i].simValue, p.SimValue()(r)) + require.Equal(t, expected[i].subspace, p.Subspace()) + } +}