diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8cdc4e..731d032a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,11 +45,11 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes * (rpc) [tharsis#1050](https://github.com/tharsis/ethermint/pull/1050) `eth_getBlockByNumber` fix on batch transactions +* (app) [tharsis#658](https://github.com/tharsis/ethermint/issues/658) Support simulations for the EVM. ### API Breaking -* (evm) [tharsis#1051](https://github.com/tharsis/ethermint/pull/1051) Context block height fix on TraceTx. Removes `tx_index` on `QueryTraceTxRequest` proto type. - +* (evm) [tharsis#1051](https://github.com/tharsis/ethermint/pull/1051) Context block height fix on TraceTx. Removes `tx_index` on `QueryTraceTxRequest` proto type. ## [v0.13.0] - 2022-04-05 diff --git a/Makefile b/Makefile index 44cb68e2..8d794168 100755 --- a/Makefile +++ b/Makefile @@ -342,10 +342,9 @@ test-sim-nondeterminism: @go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h -test-sim-custom-genesis-fast: +test-sim-random-genesis-fast: @echo "Running custom genesis simulation..." - @echo "By default, ${HOME}/.$(ETHERMINT_DIR)/config/genesis.json will be used." - @go test -mod=readonly $(SIMAPP) -run TestFullAppSimulation -Genesis=${HOME}/.$(ETHERMINT_DIR)/config/genesis.json \ + @go test -mod=readonly $(SIMAPP) -run TestFullAppSimulation \ -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -v -timeout 24h test-sim-import-export: runsim @@ -356,10 +355,9 @@ test-sim-after-import: runsim @echo "Running application simulation-after-import. This may take several minutes..." @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 50 5 TestAppSimulationAfterImport -test-sim-custom-genesis-multi-seed: runsim +test-sim-random-genesis-multi-seed: runsim @echo "Running multi-seed custom genesis simulation..." - @echo "By default, ${HOME}/.$(ETHERMINT_DIR)/config/genesis.json will be used." - @$(BINDIR)/runsim -Genesis=${HOME}/.$(ETHERMINT_DIR)/config/genesis.json -SimAppPkg=$(SIMAPP) -ExitOnFail 400 5 TestFullAppSimulation + @$(BINDIR)/runsim -SimAppPkg=$(SIMAPP) -ExitOnFail 400 5 TestFullAppSimulation test-sim-multi-seed-long: runsim @echo "Running long multi-seed application simulation. This may take awhile!" diff --git a/app/app.go b/app/app.go index 3517ed4f..43120044 100644 --- a/app/app.go +++ b/app/app.go @@ -536,7 +536,8 @@ func NewEthermintApp( // NOTE: this is not required apps that don't use the simulator for fuzz testing // transactions app.sm = module.NewSimulationManager( - auth.NewAppModule(appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts), + // Use custom RandomGenesisAccounts so that auth module could create random EthAccounts in genesis state when genesis.json not specified + auth.NewAppModule(appCodec, app.AccountKeeper, RandomGenesisAccounts), bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper), capability.NewAppModule(appCodec, *app.CapabilityKeeper), gov.NewAppModule(appCodec, app.GovKeeper, app.AccountKeeper, app.BankKeeper), diff --git a/app/simulation_test.go b/app/simulation_test.go index f3c91a9d..98ef0222 100644 --- a/app/simulation_test.go +++ b/app/simulation_test.go @@ -15,7 +15,6 @@ import ( "github.com/cosmos/cosmos-sdk/simapp/params" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" - simtypes "github.com/cosmos/cosmos-sdk/types/simulation" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" @@ -72,6 +71,8 @@ func TestFullAppSimulation(t *testing.T) { } require.NoError(t, err, "simulation setup failed") + config.ChainID = SimAppChainID + defer func() { db.Close() require.NoError(t, os.RemoveAll(dir)) @@ -85,8 +86,8 @@ func TestFullAppSimulation(t *testing.T) { t, os.Stdout, app.BaseApp, - simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), - simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + StateFn(app.AppCodec(), app.SimulationManager()), + RandomAccounts, // Replace with own random account function if using keys other than secp256k1 simapp.SimulationOperations(app, app.AppCodec(), config), app.ModuleAccountAddrs(), config, @@ -110,6 +111,8 @@ func TestAppImportExport(t *testing.T) { } require.NoError(t, err, "simulation setup failed") + config.ChainID = SimAppChainID + defer func() { db.Close() require.NoError(t, os.RemoveAll(dir)) @@ -123,8 +126,8 @@ func TestAppImportExport(t *testing.T) { t, os.Stdout, app.BaseApp, - simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), - simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + StateFn(app.AppCodec(), app.SimulationManager()), + RandomAccounts, // Replace with own random account function if using keys other than secp256k1 simapp.SimulationOperations(app, app.AppCodec(), config), app.ModuleAccountAddrs(), config, @@ -163,8 +166,8 @@ func TestAppImportExport(t *testing.T) { err = json.Unmarshal(exported.AppState, &genesisState) require.NoError(t, err) - ctxA := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) - ctxB := newApp.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) + ctxA := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight(), ChainID: SimAppChainID}) + ctxB := newApp.NewContext(true, tmproto.Header{Height: app.LastBlockHeight(), ChainID: SimAppChainID}) newApp.mm.InitGenesis(ctxB, app.AppCodec(), genesisState) newApp.StoreConsensusParams(ctxB, exported.ConsensusParams) @@ -210,6 +213,8 @@ func TestAppSimulationAfterImport(t *testing.T) { } require.NoError(t, err, "simulation setup failed") + config.ChainID = SimAppChainID + defer func() { db.Close() require.NoError(t, os.RemoveAll(dir)) @@ -223,8 +228,8 @@ func TestAppSimulationAfterImport(t *testing.T) { t, os.Stdout, app.BaseApp, - simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), - simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + StateFn(app.AppCodec(), app.SimulationManager()), + RandomAccounts, // Replace with own random account function if using keys other than secp256k1 simapp.SimulationOperations(app, app.AppCodec(), config), app.ModuleAccountAddrs(), config, @@ -264,6 +269,7 @@ func TestAppSimulationAfterImport(t *testing.T) { require.Equal(t, appName, newApp.Name()) newApp.InitChain(abci.RequestInitChain{ + ChainId: SimAppChainID, AppStateBytes: exported.AppState, }) @@ -271,8 +277,8 @@ func TestAppSimulationAfterImport(t *testing.T) { t, os.Stdout, newApp.BaseApp, - simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), - simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + StateFn(app.AppCodec(), app.SimulationManager()), + RandomAccounts, // Replace with own random account function if using keys other than secp256k1 simapp.SimulationOperations(newApp, newApp.AppCodec(), config), app.ModuleAccountAddrs(), config, @@ -322,8 +328,8 @@ func TestAppStateDeterminism(t *testing.T) { t, os.Stdout, app.BaseApp, - simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), - simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + StateFn(app.AppCodec(), app.SimulationManager()), + RandomAccounts, // Replace with own random account function if using keys other than secp256k1 simapp.SimulationOperations(app, app.AppCodec(), config), app.ModuleAccountAddrs(), config, diff --git a/app/test_helpers.go b/app/test_helpers.go deleted file mode 100644 index f4b12654..00000000 --- a/app/test_helpers.go +++ /dev/null @@ -1,64 +0,0 @@ -package app - -import ( - "encoding/json" - "time" - - "github.com/cosmos/cosmos-sdk/simapp" - "github.com/tharsis/ethermint/encoding" - - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/log" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - tmtypes "github.com/tendermint/tendermint/types" - dbm "github.com/tendermint/tm-db" -) - -// DefaultConsensusParams defines the default Tendermint consensus params used in -// EthermintApp testing. -var DefaultConsensusParams = &abci.ConsensusParams{ - Block: &abci.BlockParams{ - MaxBytes: 200000, - MaxGas: -1, // no limit - }, - Evidence: &tmproto.EvidenceParams{ - MaxAgeNumBlocks: 302400, - MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration - MaxBytes: 10000, - }, - Validator: &tmproto.ValidatorParams{ - PubKeyTypes: []string{ - tmtypes.ABCIPubKeyTypeEd25519, - }, - }, -} - -// Setup initializes a new EthermintApp. A Nop logger is set in EthermintApp. -func Setup(isCheckTx bool, patchGenesis func(*EthermintApp, simapp.GenesisState) simapp.GenesisState) *EthermintApp { - db := dbm.NewMemDB() - app := NewEthermintApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, DefaultNodeHome, 5, encoding.MakeConfig(ModuleBasics), simapp.EmptyAppOptions{}) - if !isCheckTx { - // init chain must be called to stop deliverState from being nil - genesisState := NewDefaultGenesisState() - if patchGenesis != nil { - genesisState = patchGenesis(app, genesisState) - } - - stateBytes, err := json.MarshalIndent(genesisState, "", " ") - if err != nil { - panic(err) - } - - // Initialize the chain - app.InitChain( - abci.RequestInitChain{ - ChainId: "ethermint_9000-1", - Validators: []abci.ValidatorUpdate{}, - ConsensusParams: DefaultConsensusParams, - AppStateBytes: stateBytes, - }, - ) - } - - return app -} diff --git a/app/utils.go b/app/utils.go new file mode 100644 index 00000000..b9f38312 --- /dev/null +++ b/app/utils.go @@ -0,0 +1,169 @@ +package app + +import ( + "encoding/json" + "math/rand" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/types/module" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/tharsis/ethermint/encoding" + ethermint "github.com/tharsis/ethermint/types" + evmtypes "github.com/tharsis/ethermint/x/evm/types" + + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" + "github.com/tharsis/ethermint/crypto/ethsecp256k1" +) + +// DefaultConsensusParams defines the default Tendermint consensus params used in +// EthermintApp testing. +var DefaultConsensusParams = &abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxBytes: 200000, + MaxGas: -1, // no limit + }, + Evidence: &tmproto.EvidenceParams{ + MaxAgeNumBlocks: 302400, + MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration + MaxBytes: 10000, + }, + Validator: &tmproto.ValidatorParams{ + PubKeyTypes: []string{ + tmtypes.ABCIPubKeyTypeEd25519, + }, + }, +} + +// Setup initializes a new EthermintApp. A Nop logger is set in EthermintApp. +func Setup(isCheckTx bool, patchGenesis func(*EthermintApp, simapp.GenesisState) simapp.GenesisState) *EthermintApp { + db := dbm.NewMemDB() + app := NewEthermintApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, DefaultNodeHome, 5, encoding.MakeConfig(ModuleBasics), simapp.EmptyAppOptions{}) + if !isCheckTx { + // init chain must be called to stop deliverState from being nil + genesisState := NewDefaultGenesisState() + if patchGenesis != nil { + genesisState = patchGenesis(app, genesisState) + } + + stateBytes, err := json.MarshalIndent(genesisState, "", " ") + if err != nil { + panic(err) + } + + // Initialize the chain + app.InitChain( + abci.RequestInitChain{ + ChainId: "ethermint_9000-1", + Validators: []abci.ValidatorUpdate{}, + ConsensusParams: DefaultConsensusParams, + AppStateBytes: stateBytes, + }, + ) + } + + return app +} + +// RandomGenesisAccounts is used by the auth module to create random genesis accounts in simulation when a genesis.json is not specified. +// In contrast, the default auth module's RandomGenesisAccounts implementation creates only base accounts and vestings accounts. +func RandomGenesisAccounts(simState *module.SimulationState) authtypes.GenesisAccounts { + emptyCodeHash := crypto.Keccak256(nil) + genesisAccs := make(authtypes.GenesisAccounts, len(simState.Accounts)) + for i, acc := range simState.Accounts { + bacc := authtypes.NewBaseAccountWithAddress(acc.Address) + + ethacc := ðermint.EthAccount{ + BaseAccount: bacc, + CodeHash: common.BytesToHash(emptyCodeHash).String(), + } + genesisAccs[i] = ethacc + } + + return genesisAccs +} + +// RandomAccounts creates random accounts with an ethsecp256k1 private key +// TODO: replace secp256k1.GenPrivKeyFromSecret() with similar function in go-ethereum +func RandomAccounts(r *rand.Rand, n int) []simtypes.Account { + accs := make([]simtypes.Account, n) + + for i := 0; i < n; i++ { + // don't need that much entropy for simulation + privkeySeed := make([]byte, 15) + _, _ = r.Read(privkeySeed) + + prv := secp256k1.GenPrivKeyFromSecret(privkeySeed) + ethPrv := ðsecp256k1.PrivKey{} + _ = ethPrv.UnmarshalAmino(prv.Bytes()) + accs[i].PrivKey = ethPrv + accs[i].PubKey = accs[i].PrivKey.PubKey() + accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address()) + + accs[i].ConsKey = ed25519.GenPrivKeyFromSecret(privkeySeed) + } + + return accs +} + +// StateFn returns the initial application state using a genesis or the simulation parameters. +// It is a wrapper of simapp.AppStateFn to replace evm param EvmDenom with staking param BondDenom. +func StateFn(cdc codec.JSONCodec, simManager *module.SimulationManager) simtypes.AppStateFn { + return func(r *rand.Rand, accs []simtypes.Account, config simtypes.Config, + ) (appState json.RawMessage, simAccs []simtypes.Account, chainID string, genesisTimestamp time.Time) { + appStateFn := simapp.AppStateFn(cdc, simManager) + appState, simAccs, chainID, genesisTimestamp = appStateFn(r, accs, config) + + rawState := make(map[string]json.RawMessage) + err := json.Unmarshal(appState, &rawState) + if err != nil { + panic(err) + } + + stakingStateBz, ok := rawState[stakingtypes.ModuleName] + if !ok { + panic("staking genesis state is missing") + } + + stakingState := new(stakingtypes.GenesisState) + cdc.MustUnmarshalJSON(stakingStateBz, stakingState) + + // we should get the BondDenom and make it the evmdenom. + // thus simulation accounts could have positive amount of gas token. + bondDenom := stakingState.Params.BondDenom + + evmStateBz, ok := rawState[evmtypes.ModuleName] + if !ok { + panic("staking genesis state is missing") + } + + evmState := new(evmtypes.GenesisState) + cdc.MustUnmarshalJSON(evmStateBz, evmState) + + // we should replace the EvmDenom with BondDenom + evmState.Params.EvmDenom = bondDenom + + // change appState back + rawState[evmtypes.ModuleName] = cdc.MustMarshalJSON(evmState) + + // replace appstate + appState, err = json.Marshal(rawState) + if err != nil { + panic(err) + } + return appState, simAccs, chainID, genesisTimestamp + } +} diff --git a/app/utils_test.go b/app/utils_test.go new file mode 100644 index 00000000..1ad1b16e --- /dev/null +++ b/app/utils_test.go @@ -0,0 +1,55 @@ +package app + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + 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" + + ethermint "github.com/tharsis/ethermint/types" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var maxTestingAccounts = 100 +var seed = int64(233) + +func TestRandomGenesisAccounts(t *testing.T) { + r := rand.New(rand.NewSource(seed)) + accs := RandomAccounts(r, rand.Intn(maxTestingAccounts)) + + encodingConfig := MakeEncodingConfig() + appCodec := encodingConfig.Marshaler + cdc := encodingConfig.Amino + + paramsKeeper := initParamsKeeper(appCodec, cdc, sdk.NewKVStoreKey(paramstypes.StoreKey), sdk.NewTransientStoreKey(paramstypes.StoreKey)) + subSpace, find := paramsKeeper.GetSubspace(authtypes.ModuleName) + require.True(t, find) + accountKeeper := authkeeper.NewAccountKeeper( + appCodec, sdk.NewKVStoreKey(authtypes.StoreKey), subSpace, ethermint.ProtoAccount, maccPerms, + ) + authModule := auth.NewAppModule(appCodec, accountKeeper, RandomGenesisAccounts) + + genesisState := simapp.NewDefaultGenesisState(appCodec) + simState := &module.SimulationState{Accounts: accs, Cdc: appCodec, Rand: r, GenState: genesisState} + authModule.GenerateGenesisState(simState) + + authStateBz, find := genesisState[authtypes.ModuleName] + require.True(t, find) + + authState := new(authtypes.GenesisState) + appCodec.MustUnmarshalJSON(authStateBz, authState) + accounts, err := authtypes.UnpackAccounts(authState.Accounts) + require.NoError(t, err) + for _, acc := range accounts { + _, ok := acc.(ethermint.EthAccountI) + require.True(t, ok) + } +} diff --git a/x/evm/module.go b/x/evm/module.go index ff07071e..1335b263 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -170,11 +170,13 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw // RandomizedParams creates randomized evm param changes for the simulator. func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { - return nil + return simulation.ParamChanges(r) } // RegisterStoreDecoder registers a decoder for evm module's types -func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {} +func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore() +} // ProposalContents doesn't return any content functions for governance proposals. func (AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { @@ -188,5 +190,7 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) { // WeightedOperations returns the all the evm module operations with their respective weights. func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { - return nil + return simulation.WeightedOperations( + simState.AppParams, simState.Cdc, am.ak, am.keeper, + ) } diff --git a/x/evm/simulation/decoder.go b/x/evm/simulation/decoder.go new file mode 100644 index 00000000..e09e870c --- /dev/null +++ b/x/evm/simulation/decoder.go @@ -0,0 +1,31 @@ +package simulation + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/types/kv" + "github.com/ethereum/go-ethereum/common" + "github.com/tharsis/ethermint/x/evm/types" +) + +// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's +// 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() + + return fmt.Sprintf("%v\n%v", storageHashA, storageHashB) + case bytes.Equal(kvA.Key[:1], types.KeyPrefixCode): + codeHashA := common.BytesToHash(kvA.Value).Hex() + codeHashB := common.BytesToHash(kvB.Value).Hex() + + return fmt.Sprintf("%v\n%v", codeHashA, codeHashB) + default: + panic(fmt.Sprintf("invalid evm key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/evm/simulation/genesis.go b/x/evm/simulation/genesis.go index 74c2eebb..17f65cdb 100644 --- a/x/evm/simulation/genesis.go +++ b/x/evm/simulation/genesis.go @@ -3,18 +3,26 @@ package simulation import ( "encoding/json" "fmt" + "math/rand" "github.com/cosmos/cosmos-sdk/types/module" "github.com/tharsis/ethermint/x/evm/types" ) +// GenExtraEIPs randomly generates specific extra eips or not. +func genExtraEIPs(r *rand.Rand) []int64 { + var extraEIPs []int64 + if r.Uint32()%2 == 0 { + extraEIPs = []int64{1344, 1884, 2200, 2929, 3198, 3529} + } + return extraEIPs +} + // RandomizedGenState generates a random GenesisState for nft func RandomizedGenState(simState *module.SimulationState) { - params := types.NewParams(types.DefaultEVMDenom, true, true, types.DefaultChainConfig()) - if simState.Rand.Uint32()%2 == 0 { - params = types.NewParams(types.DefaultEVMDenom, true, true, types.DefaultChainConfig(), 1344, 1884, 2200, 2929, 3198, 3529) - } + extraEIPs := genExtraEIPs(simState.Rand) + params := types.NewParams(types.DefaultEVMDenom, true, true, types.DefaultChainConfig(), extraEIPs...) evmGenesis := types.NewGenesisState(params, []types.GenesisAccount{}) bz, err := json.MarshalIndent(evmGenesis, "", " ") diff --git a/x/evm/simulation/operations.go b/x/evm/simulation/operations.go new file mode 100644 index 00000000..7709320f --- /dev/null +++ b/x/evm/simulation/operations.go @@ -0,0 +1,299 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/big" + "math/rand" + "time" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + sdktx "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/x/auth/tx" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/cosmos/cosmos-sdk/client" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "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" +) + +const ( + /* #nosec */ + OpWeightMsgEthSimpleTransfer = "op_weight_msg_eth_simple_transfer" + /* #nosec */ + OpWeightMsgEthCreateContract = "op_weight_msg_eth_create_contract" + /* #nosec */ + OpWeightMsgEthCallContract = "op_weight_msg_eth_call_contract" +) + +const ( + WeightMsgEthSimpleTransfer = 50 + WeightMsgEthCreateContract = 50 +) + +var ErrNoEnoughBalance = fmt.Errorf("no enough balance") + +var maxWaitSeconds = 10 + +type simulateContext struct { + context sdk.Context + bapp *baseapp.BaseApp + rand *rand.Rand + keeper *keeper.Keeper +} + +// WeightedOperations generate Two kinds of operations: SimulateEthSimpleTransfer, SimulateEthCreateContract. +// Contract call operations work as the future operations of SimulateEthCreateContract. +func WeightedOperations( + appParams simtypes.AppParams, cdc codec.JSONCodec, ak types.AccountKeeper, k *keeper.Keeper, +) simulation.WeightedOperations { + var ( + weightMsgEthSimpleTransfer int + weightMsgEthCreateContract int + ) + + appParams.GetOrGenerate(cdc, OpWeightMsgEthSimpleTransfer, &weightMsgEthSimpleTransfer, nil, + func(_ *rand.Rand) { + weightMsgEthSimpleTransfer = WeightMsgEthSimpleTransfer + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightMsgEthCreateContract, &weightMsgEthCreateContract, nil, + func(_ *rand.Rand) { + weightMsgEthCreateContract = WeightMsgEthCreateContract + }, + ) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgEthSimpleTransfer, + SimulateEthSimpleTransfer(ak, k), + ), + simulation.NewWeightedOperation( + weightMsgEthCreateContract, + SimulateEthCreateContract(ak, k), + ), + } +} + +// SimulateEthSimpleTransfer simulate simple eth account transferring gas token. +// It randomly choose sender, recipient and transferable amount. +// Other tx details like nonce, gasprice, gaslimit are calculated to get valid value. +func SimulateEthSimpleTransfer(ak types.AccountKeeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, bapp *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + var recipient simtypes.Account + if r.Intn(2) == 1 { + recipient, _ = simtypes.RandomAcc(r, accs) + } else { + recipient = simtypes.RandomAccounts(r, 1)[0] + } + from := common.BytesToAddress(simAccount.Address) + to := common.BytesToAddress(recipient.Address) + + simulateContext := &simulateContext{ctx, bapp, r, k} + + return SimulateEthTx(simulateContext, &from, &to, nil, (*hexutil.Bytes)(&[]byte{}), simAccount.PrivKey, nil) + } +} + +// SimulateEthCreateContract simulate create an ERC20 contract. +// It makes operationSimulateEthCallContract the future operations of SimulateEthCreateContract to ensure valid contract call. +func SimulateEthCreateContract(ak types.AccountKeeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, bapp *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + + from := common.BytesToAddress(simAccount.Address) + nonce := k.GetNonce(ctx, from) + + ctorArgs, err := types.ERC20Contract.ABI.Pack("", from, sdk.NewIntWithDecimal(1000, 18).BigInt()) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEthereumTx, "can not pack owner and supply"), nil, err + } + data := types.ERC20Contract.Bin + data = append(data, ctorArgs...) + + simulateContext := &simulateContext{ctx, bapp, r, k} + + fops := make([]simtypes.FutureOperation, 1) + whenCall := ctx.BlockHeader().Time.Add(time.Duration(r.Intn(maxWaitSeconds)+1) * time.Second) + contractAddr := crypto.CreateAddress(from, nonce) + var tokenReceipient simtypes.Account + if r.Intn(2) == 1 { + tokenReceipient, _ = simtypes.RandomAcc(r, accs) + } else { + tokenReceipient = simtypes.RandomAccounts(r, 1)[0] + } + receipientAddr := common.BytesToAddress(tokenReceipient.Address) + fops[0] = simtypes.FutureOperation{ + BlockTime: whenCall, + Op: operationSimulateEthCallContract(k, &contractAddr, &receipientAddr, nil), + } + return SimulateEthTx(simulateContext, &from, nil, nil, (*hexutil.Bytes)(&data), simAccount.PrivKey, fops) + } +} + +// operationSimulateEthCallContract simulate calling an contract. +// It is always calling an ERC20 contract. +func operationSimulateEthCallContract(k *keeper.Keeper, contractAddr, to *common.Address, amount *big.Int) simtypes.Operation { + return func( + r *rand.Rand, bapp *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + + from := common.BytesToAddress(simAccount.Address) + + ctorArgs, err := types.ERC20Contract.ABI.Pack("transfer", to, amount) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEthereumTx, "can not pack method and args"), nil, err + } + data := types.ERC20Contract.Bin + data = append(data, ctorArgs...) + + simulateContext := &simulateContext{ctx, bapp, r, k} + + return SimulateEthTx(simulateContext, &from, contractAddr, nil, (*hexutil.Bytes)(&data), simAccount.PrivKey, nil) + } +} + +// SimulateEthTx creates valid ethereum tx and pack it as cosmos tx, and deliver it. +func SimulateEthTx( + ctx *simulateContext, from, to *common.Address, amount *big.Int, data *hexutil.Bytes, prv cryptotypes.PrivKey, fops []simtypes.FutureOperation, +) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + ethTx, err := CreateRandomValidEthTx(ctx, from, nil, nil, data) + if err == ErrNoEnoughBalance { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEthereumTx, "no enough balance"), nil, nil + } + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEthereumTx, "can not create valid eth tx"), nil, err + } + + txConfig := encoding.MakeConfig(module.NewBasicManager()).TxConfig + txBuilder := txConfig.NewTxBuilder() + signedTx, err := GetSignedTx(ctx, txBuilder, ethTx, prv) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEthereumTx, "can not sign ethereum tx"), nil, err + } + + _, _, err = ctx.bapp.Deliver(txConfig.TxEncoder(), signedTx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEthereumTx, "failed to deliver tx"), nil, err + } + + return simtypes.OperationMsg{}, fops, nil +} + +// 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) + if err != nil { + return nil, err + } + gasLimit := estimateGas + uint64(ctx.rand.Intn(int(sdktx.MaxGasWanted-estimateGas))) + ethChainID := ctx.keeper.ChainID() + chainConfig := ctx.keeper.GetParams(ctx.context).ChainConfig.EthereumConfig(ethChainID) + gasPrice := ctx.keeper.BaseFee(ctx.context, chainConfig) + gasFeeCap := new(big.Int).Add(gasPrice, big.NewInt(int64(ctx.rand.Int()))) + gasTipCap := big.NewInt(int64(ctx.rand.Int())) + nonce := ctx.keeper.GetNonce(ctx.context, *from) + + if amount == nil { + amount, err = RandomTransferableAmount(ctx, *from, gasLimit, gasFeeCap) + if err != nil { + return nil, err + } + } + + ethTx = types.NewTx(ethChainID, nonce, to, amount, gasLimit, gasPrice, gasFeeCap, gasTipCap, *data, nil) + ethTx.From = from.String() + return ethTx, nil +} + +// EstimateGas estimates the gas used by quering the keeper. +func EstimateGas(ctx *simulateContext, from, to *common.Address, data *hexutil.Bytes) (gas uint64, err error) { + args, err := json.Marshal(&types.TransactionArgs{To: to, From: from, Data: data}) + if err != nil { + return 0, err + } + + res, err := ctx.keeper.EstimateGas(sdk.WrapSDKContext(ctx.context), &types.EthCallRequest{ + Args: args, + GasCap: config.DefaultGasCap, + }) + if err != nil { + return 0, err + } + return res.Gas, nil +} + +// 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) { + balance := ctx.keeper.GetBalance(ctx.context, address) + feeLimit := new(big.Int).Mul(gasFeeCap, big.NewInt(int64(gasLimit))) + if (feeLimit.Cmp(balance)) > 0 { + return nil, ErrNoEnoughBalance + } + spendable := new(big.Int).Sub(balance, feeLimit) + if spendable.Cmp(big.NewInt(0)) == 0 { + amount = new(big.Int).Set(spendable) + return amount, nil + } + simAmount, err := simtypes.RandPositiveInt(ctx.rand, sdk.NewIntFromBigInt(spendable)) + if err != nil { + return nil, err + } + amount = simAmount.BigInt() + return amount, nil +} + +// GetSignedTx sign the ethereum tx and packs it as a signing.Tx . +func GetSignedTx(ctx *simulateContext, txBuilder client.TxBuilder, msg *types.MsgEthereumTx, prv cryptotypes.PrivKey) (signedTx signing.Tx, err error) { + builder, ok := txBuilder.(tx.ExtensionOptionsTxBuilder) + if !ok { + return nil, fmt.Errorf("can not initiate ExtensionOptionsTxBuilder") + } + option, err := codectypes.NewAnyWithValue(&types.ExtensionOptionsEthereumTx{}) + if err != nil { + return nil, err + } + builder.SetExtensionOptions(option) + + if err := msg.Sign(ethtypes.LatestSignerForChainID(ctx.keeper.ChainID()), tests.NewSigner(prv)); err != nil { + return nil, err + } + + if err = builder.SetMsgs(msg); err != nil { + return nil, err + } + + txData, err := types.UnpackTxData(msg.Data) + if err != nil { + return nil, err + } + + fees := sdk.NewCoins(sdk.NewCoin(ctx.keeper.GetParams(ctx.context).EvmDenom, sdk.NewIntFromBigInt(txData.Fee()))) + builder.SetFeeAmount(fees) + builder.SetGasLimit(msg.GetGas()) + + signedTx = builder.GetTx() + return signedTx, nil +} diff --git a/x/evm/simulation/params.go b/x/evm/simulation/params.go new file mode 100644 index 00000000..94295a5c --- /dev/null +++ b/x/evm/simulation/params.go @@ -0,0 +1,28 @@ +package simulation + +// DONTCOVER + +import ( + "fmt" + "math/rand" + + 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, + func(r *rand.Rand) string { + return fmt.Sprintf("\"%d\"", genExtraEIPs(r)) + }, + ), + } +}