diff --git a/CHANGELOG.md b/CHANGELOG.md index b40f676d..44523f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (ante) [#1176](https://github.com/evmos/ethermint/pull/1176) Fix invalid tx hashes; Remove `Size_` field and validate `Hash`/`From` fields in ante handler, recompute eth tx hashes in JSON-RPC APIs to fix old blocks. * (deps) [#1168](https://github.com/evmos/ethermint/pull/1168) Upgrade cosmos-sdk to v0.46. +* (feemarket) [#1194](https://github.com/evmos/ethermint/pull/1194) Apply feemarket to native cosmos tx. ### API Breaking diff --git a/app/ante/ante.go b/app/ante/ante.go index 50fabbff..666cfed6 100644 --- a/app/ante/ante.go +++ b/app/ante/ante.go @@ -27,6 +27,7 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { if err := options.validate(); err != nil { return nil, err } + return func( ctx sdk.Context, tx sdk.Tx, sim bool, ) (newCtx sdk.Context, err error) { @@ -45,6 +46,9 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { case "/ethermint.types.v1.ExtensionOptionsWeb3Tx": // handle as normal Cosmos SDK tx, except signature is checked for EIP712 representation anteHandler = newCosmosAnteHandlerEip712(options) + case "/ethermint.types.v1.ExtensionOptionDynamicFeeTx": + // cosmos-sdk tx with dynamic fee extension + anteHandler = newCosmosAnteHandler(options) default: return ctx, sdkerrors.Wrapf( sdkerrors.ErrUnknownExtensionOptions, diff --git a/app/ante/ante_test.go b/app/ante/ante_test.go index b5c5e85f..580d84f2 100644 --- a/app/ante/ante_test.go +++ b/app/ante/ante_test.go @@ -305,8 +305,8 @@ func (suite AnteTestSuite) TestAnteHandler() { "success - DeliverTx EIP712 signed Cosmos Tx with MsgSend", func() sdk.Tx { from := acc.GetAddress() - amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(20))) gas := uint64(200000) + amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(100*int64(gas)))) txBuilder := suite.CreateTestEIP712TxBuilderMsgSend(from, privKey, "ethermint_9000-1", gas, amount) return txBuilder.GetTx() }, false, false, true, @@ -315,9 +315,9 @@ func (suite AnteTestSuite) TestAnteHandler() { "success - DeliverTx EIP712 signed Cosmos Tx with DelegateMsg", func() sdk.Tx { from := acc.GetAddress() - coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(20)) - amount := sdk.NewCoins(coinAmount) gas := uint64(200000) + coinAmount := sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(100*int64(gas))) + amount := sdk.NewCoins(coinAmount) txBuilder := suite.CreateTestEIP712TxBuilderMsgDelegate(from, privKey, "ethermint_9000-1", gas, amount) return txBuilder.GetTx() }, false, false, true, @@ -326,8 +326,8 @@ func (suite AnteTestSuite) TestAnteHandler() { "fails - DeliverTx EIP712 signed Cosmos Tx with wrong Chain ID", func() sdk.Tx { from := acc.GetAddress() - amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(20))) gas := uint64(200000) + amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(100*int64(gas)))) txBuilder := suite.CreateTestEIP712TxBuilderMsgSend(from, privKey, "ethermint_9002-1", gas, amount) return txBuilder.GetTx() }, false, false, false, @@ -336,8 +336,8 @@ func (suite AnteTestSuite) TestAnteHandler() { "fails - DeliverTx EIP712 signed Cosmos Tx with different gas fees", func() sdk.Tx { from := acc.GetAddress() - amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(20))) gas := uint64(200000) + amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(100*int64(gas)))) txBuilder := suite.CreateTestEIP712TxBuilderMsgSend(from, privKey, "ethermint_9001-1", gas, amount) txBuilder.SetGasLimit(uint64(300000)) txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(30)))) @@ -348,8 +348,8 @@ func (suite AnteTestSuite) TestAnteHandler() { "fails - DeliverTx EIP712 signed Cosmos Tx with empty signature", func() sdk.Tx { from := acc.GetAddress() - amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(20))) gas := uint64(200000) + amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(100*int64(gas)))) txBuilder := suite.CreateTestEIP712TxBuilderMsgSend(from, privKey, "ethermint_9001-1", gas, amount) sigsV2 := signing.SignatureV2{} txBuilder.SetSignatures(sigsV2) @@ -360,8 +360,8 @@ func (suite AnteTestSuite) TestAnteHandler() { "fails - DeliverTx EIP712 signed Cosmos Tx with invalid sequence", func() sdk.Tx { from := acc.GetAddress() - amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(20))) gas := uint64(200000) + amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(100*int64(gas)))) txBuilder := suite.CreateTestEIP712TxBuilderMsgSend(from, privKey, "ethermint_9001-1", gas, amount) nonce, err := suite.app.AccountKeeper.GetSequence(suite.ctx, acc.GetAddress()) suite.Require().NoError(err) @@ -380,8 +380,8 @@ func (suite AnteTestSuite) TestAnteHandler() { "fails - DeliverTx EIP712 signed Cosmos Tx with invalid signMode", func() sdk.Tx { from := acc.GetAddress() - amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(20))) gas := uint64(200000) + amount := sdk.NewCoins(sdk.NewCoin(evmtypes.DefaultEVMDenom, sdkmath.NewInt(100*int64(gas)))) txBuilder := suite.CreateTestEIP712TxBuilderMsgSend(from, privKey, "ethermint_9001-1", gas, amount) nonce, err := suite.app.AccountKeeper.GetSequence(suite.ctx, acc.GetAddress()) suite.Require().NoError(err) diff --git a/app/ante/eth_test.go b/app/ante/eth_test.go index 472312f2..d77b8ba8 100644 --- a/app/ante/eth_test.go +++ b/app/ante/eth_test.go @@ -9,7 +9,6 @@ import ( "github.com/evmos/ethermint/app/ante" "github.com/evmos/ethermint/server/config" "github.com/evmos/ethermint/tests" - evmkeeper "github.com/evmos/ethermint/x/evm/keeper" "github.com/evmos/ethermint/x/evm/statedb" evmtypes "github.com/evmos/ethermint/x/evm/types" @@ -227,7 +226,7 @@ func (suite AnteTestSuite) TestEthGasConsumeDecorator() { baseFee := suite.app.EvmKeeper.GetBaseFee(suite.ctx, ethCfg) suite.Require().Equal(int64(1000000000), baseFee.Int64()) - gasPrice := new(big.Int).Add(baseFee, evmkeeper.DefaultPriorityReduction.BigInt()) + gasPrice := new(big.Int).Add(baseFee, evmtypes.DefaultPriorityReduction.BigInt()) tx2GasLimit := uint64(1000000) tx2 := evmtypes.NewTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), tx2GasLimit, gasPrice, nil, nil, nil, ðtypes.AccessList{{Address: addr, StorageKeys: nil}}) @@ -236,8 +235,8 @@ func (suite AnteTestSuite) TestEthGasConsumeDecorator() { dynamicFeeTx := evmtypes.NewTxContract(suite.app.EvmKeeper.ChainID(), 1, big.NewInt(10), tx2GasLimit, nil, // gasPrice - new(big.Int).Add(baseFee, big.NewInt(evmkeeper.DefaultPriorityReduction.Int64()*2)), // gasFeeCap - evmkeeper.DefaultPriorityReduction.BigInt(), // gasTipCap + new(big.Int).Add(baseFee, big.NewInt(evmtypes.DefaultPriorityReduction.Int64()*2)), // gasFeeCap + evmtypes.DefaultPriorityReduction.BigInt(), // gasTipCap nil, ðtypes.AccessList{{Address: addr, StorageKeys: nil}}) dynamicFeeTx.From = addr.Hex() dynamicFeeTxPriority := int64(1) diff --git a/app/ante/fee_checker.go b/app/ante/fee_checker.go new file mode 100644 index 00000000..ddda0345 --- /dev/null +++ b/app/ante/fee_checker.go @@ -0,0 +1,142 @@ +package ante + +import ( + "fmt" + "math" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + ethermint "github.com/evmos/ethermint/types" + "github.com/evmos/ethermint/x/evm/types" +) + +// NewDynamicFeeChecker returns a `TxFeeChecker` that applies a dynamic fee to +// Cosmos txs using the EIP-1559 fee market logic. +// This can be called in both CheckTx and deliverTx modes. +// a) feeCap = tx.fees / tx.gas +// b) tipFeeCap = tx.MaxPriorityPrice (default) or MaxInt64 +// - when `ExtensionOptionDynamicFeeTx` is omitted, `tipFeeCap` defaults to `MaxInt64`. +// - when london hardfork is not enabled, it fallbacks to SDK default behavior (validator min-gas-prices). +// - Tx priority is set to `effectiveGasPrice / DefaultPriorityReduction`. +func NewDynamicFeeChecker(k DynamicFeeEVMKeeper) authante.TxFeeChecker { + return func(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, int64, error) { + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return nil, 0, fmt.Errorf("tx must be a FeeTx") + } + + if ctx.BlockHeight() == 0 { + // genesis transactions: fallback to min-gas-price logic + return checkTxFeeWithValidatorMinGasPrices(ctx, feeTx) + } + + params := k.GetParams(ctx) + denom := params.EvmDenom + ethCfg := params.ChainConfig.EthereumConfig(k.ChainID()) + + baseFee := k.GetBaseFee(ctx, ethCfg) + if baseFee == nil { + // london hardfork is not enabled: fallback to min-gas-prices logic + return checkTxFeeWithValidatorMinGasPrices(ctx, feeTx) + } + + // default to `MaxInt64` when there's no extension option. + maxPriorityPrice := sdkmath.NewInt(math.MaxInt64) + + // get the priority tip cap from the extension option. + if hasExtOptsTx, ok := tx.(authante.HasExtensionOptionsTx); ok { + for _, opt := range hasExtOptsTx.GetExtensionOptions() { + if extOpt, ok := opt.GetCachedValue().(*ethermint.ExtensionOptionDynamicFeeTx); ok { + maxPriorityPrice = extOpt.MaxPriorityPrice + break + } + } + } + + gas := feeTx.GetGas() + feeCoins := feeTx.GetFee() + fee := feeCoins.AmountOfNoDenomValidation(denom) + + feeCap := fee.Quo(sdkmath.NewIntFromUint64(gas)) + baseFeeInt := sdkmath.NewIntFromBigInt(baseFee) + + if feeCap.LT(baseFeeInt) { + return nil, 0, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient gas prices; got: %s required: %s", feeCap, baseFeeInt) + } + + // calculate the effective gas price using the EIP-1559 logic. + effectivePrice := sdkmath.NewIntFromBigInt(types.EffectiveGasPrice(baseFeeInt.BigInt(), feeCap.BigInt(), maxPriorityPrice.BigInt())) + + // NOTE: create a new coins slice without having to validate the denom + effectiveFee := sdk.Coins{ + { + Denom: denom, + Amount: effectivePrice.Mul(sdkmath.NewIntFromUint64(gas)), + }, + } + + bigPriority := effectivePrice.Sub(baseFeeInt).Quo(types.DefaultPriorityReduction) + priority := int64(math.MaxInt64) + + if bigPriority.IsInt64() { + priority = bigPriority.Int64() + } + + return effectiveFee, priority, nil + } +} + +// checkTxFeeWithValidatorMinGasPrices implements the default fee logic, where the minimum price per +// unit of gas is fixed and set by each validator, and the tx priority is computed from the gas price. +func checkTxFeeWithValidatorMinGasPrices(ctx sdk.Context, tx sdk.FeeTx) (sdk.Coins, int64, error) { + feeCoins := tx.GetFee() + gas := tx.GetGas() + minGasPrices := ctx.MinGasPrices() + + // Ensure that the provided fees meet a minimum threshold for the validator, + // if this is a CheckTx. This is only for local mempool purposes, and thus + // is only ran on check tx. + if ctx.IsCheckTx() && !minGasPrices.IsZero() { + requiredFees := make(sdk.Coins, len(minGasPrices)) + + // Determine the required fees by multiplying each required minimum gas + // price by the gas limit, where fee = ceil(minGasPrice * gasLimit). + glDec := sdk.NewDec(int64(gas)) + + for i, gp := range minGasPrices { + fee := gp.Amount.Mul(glDec) + requiredFees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) + } + + if !feeCoins.IsAnyGTE(requiredFees) { + return nil, 0, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees) + } + } + + priority := getTxPriority(feeCoins) + return feeCoins, priority, nil +} + +// getTxPriority returns a naive tx priority based on the amount of the smallest denomination of the fee +// provided in a transaction. +func getTxPriority(fees sdk.Coins) int64 { + var priority int64 + + for _, fee := range fees { + amt := fee.Amount.Quo(types.DefaultPriorityReduction) + p := int64(math.MaxInt64) + + if amt.IsInt64() { + p = amt.Int64() + } + + if priority == 0 || p < priority { + priority = p + } + } + + return priority +} diff --git a/app/ante/fee_checker_test.go b/app/ante/fee_checker_test.go new file mode 100644 index 00000000..95a4b92a --- /dev/null +++ b/app/ante/fee_checker_test.go @@ -0,0 +1,219 @@ +package ante + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + "github.com/ethereum/go-ethereum/params" + "github.com/evmos/ethermint/encoding" + ethermint "github.com/evmos/ethermint/types" + "github.com/evmos/ethermint/x/evm/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" +) + +var _ DynamicFeeEVMKeeper = MockEVMKeeper{} + +type MockEVMKeeper struct { + BaseFee *big.Int + EnableLondonHF bool +} + +func (m MockEVMKeeper) GetBaseFee(ctx sdk.Context, ethCfg *params.ChainConfig) *big.Int { + if m.EnableLondonHF { + return m.BaseFee + } + return nil +} + +func (m MockEVMKeeper) GetParams(ctx sdk.Context) evmtypes.Params { + return evmtypes.DefaultParams() +} + +func (m MockEVMKeeper) ChainID() *big.Int { + return big.NewInt(9000) +} + +func TestSDKTxFeeChecker(t *testing.T) { + // testCases: + // fallback + // genesis tx + // checkTx, validate with min-gas-prices + // deliverTx, no validation + // dynamic fee + // with extension option + // without extension option + // london hardfork enableness + encodingConfig := encoding.MakeConfig(module.NewBasicManager()) + minGasPrices := sdk.NewDecCoins(sdk.NewDecCoin("aphoton", sdk.NewInt(10))) + + genesisCtx := sdk.NewContext(nil, tmproto.Header{}, false, log.NewNopLogger()) + checkTxCtx := sdk.NewContext(nil, tmproto.Header{Height: 1}, true, log.NewNopLogger()).WithMinGasPrices(minGasPrices) + deliverTxCtx := sdk.NewContext(nil, tmproto.Header{Height: 1}, false, log.NewNopLogger()) + + testCases := []struct { + name string + ctx sdk.Context + keeper DynamicFeeEVMKeeper + buildTx func() sdk.Tx + expFees string + expPriority int64 + expSuccess bool + }{ + { + "success, genesis tx", + genesisCtx, + MockEVMKeeper{}, + func() sdk.Tx { + return encodingConfig.TxConfig.NewTxBuilder().GetTx() + }, + "", + 0, + true, + }, + { + "fail, min-gas-prices", + checkTxCtx, + MockEVMKeeper{}, + func() sdk.Tx { + return encodingConfig.TxConfig.NewTxBuilder().GetTx() + }, + "", + 0, + false, + }, + { + "success, min-gas-prices", + checkTxCtx, + MockEVMKeeper{}, + func() sdk.Tx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("aphoton", sdk.NewInt(10)))) + return txBuilder.GetTx() + }, + "10aphoton", + 0, + true, + }, + { + "success, min-gas-prices deliverTx", + deliverTxCtx, + MockEVMKeeper{}, + func() sdk.Tx { + return encodingConfig.TxConfig.NewTxBuilder().GetTx() + }, + "", + 0, + true, + }, + { + "fail, dynamic fee", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(1), + }, + func() sdk.Tx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + txBuilder.SetGasLimit(1) + return txBuilder.GetTx() + }, + "", + 0, + false, + }, + { + "success, dynamic fee", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(10), + }, + func() sdk.Tx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("aphoton", sdk.NewInt(10)))) + return txBuilder.GetTx() + }, + "10aphoton", + 0, + true, + }, + { + "success, dynamic fee priority", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(10), + }, + func() sdk.Tx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("aphoton", sdk.NewInt(10).Mul(types.DefaultPriorityReduction).Add(sdk.NewInt(10))))) + return txBuilder.GetTx() + }, + "10000010aphoton", + 10, + true, + }, + { + "success, dynamic fee empty tipFeeCap", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(10), + }, + func() sdk.Tx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("aphoton", sdk.NewInt(10).Mul(types.DefaultPriorityReduction)))) + + option, err := codectypes.NewAnyWithValue(ðermint.ExtensionOptionDynamicFeeTx{}) + require.NoError(t, err) + txBuilder.SetExtensionOptions(option) + return txBuilder.GetTx() + }, + "10aphoton", + 0, + true, + }, + { + "success, dynamic fee tipFeeCap", + deliverTxCtx, + MockEVMKeeper{ + EnableLondonHF: true, BaseFee: big.NewInt(10), + }, + func() sdk.Tx { + txBuilder := encodingConfig.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + txBuilder.SetGasLimit(1) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("aphoton", sdk.NewInt(10).Mul(types.DefaultPriorityReduction).Add(sdk.NewInt(10))))) + + option, err := codectypes.NewAnyWithValue(ðermint.ExtensionOptionDynamicFeeTx{ + MaxPriorityPrice: sdk.NewInt(5).Mul(types.DefaultPriorityReduction), + }) + require.NoError(t, err) + txBuilder.SetExtensionOptions(option) + return txBuilder.GetTx() + }, + "5000010aphoton", + 5, + true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fees, priority, err := NewDynamicFeeChecker(tc.keeper)(tc.ctx, tc.buildTx()) + if tc.expSuccess { + require.Equal(t, tc.expFees, fees.String()) + require.Equal(t, tc.expPriority, priority) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/app/ante/interfaces.go b/app/ante/interfaces.go index 83be8b88..a39e7b2c 100644 --- a/app/ante/interfaces.go +++ b/app/ante/interfaces.go @@ -14,17 +14,22 @@ import ( feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" ) +// DynamicFeeEVMKeeper is a subset of EVMKeeper interface that supports dynamic fee checker +type DynamicFeeEVMKeeper interface { + ChainID() *big.Int + GetParams(ctx sdk.Context) evmtypes.Params + GetBaseFee(ctx sdk.Context, ethCfg *params.ChainConfig) *big.Int +} + // EVMKeeper defines the expected keeper interface used on the Eth AnteHandler type EVMKeeper interface { statedb.Keeper + DynamicFeeEVMKeeper - ChainID() *big.Int - GetParams(ctx sdk.Context) evmtypes.Params NewEVM(ctx sdk.Context, msg core.Message, cfg *evmtypes.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB) *vm.EVM DeductTxCostsFromUserBalance( ctx sdk.Context, msgEthTx evmtypes.MsgEthereumTx, txData evmtypes.TxData, denom string, homestead, istanbul, london bool, ) (fees sdk.Coins, priority int64, err error) - GetBaseFee(ctx sdk.Context, ethCfg *params.ChainConfig) *big.Int GetBalance(ctx sdk.Context, addr common.Address) *big.Int ResetTransientGasUsed(ctx sdk.Context) GetTxIndexTransient(ctx sdk.Context) uint64 diff --git a/app/app.go b/app/app.go index 9d5e3d72..da44c574 100644 --- a/app/app.go +++ b/app/app.go @@ -600,15 +600,17 @@ func NewEthermintApp( // use Ethermint's custom AnteHandler func (app *EthermintApp) setAnteHandler(txConfig client.TxConfig, maxGasWanted uint64) { anteHandler, err := ante.NewAnteHandler(ante.HandlerOptions{ - AccountKeeper: app.AccountKeeper, - BankKeeper: app.BankKeeper, - SignModeHandler: txConfig.SignModeHandler(), - FeegrantKeeper: app.FeeGrantKeeper, - SigGasConsumer: ante.DefaultSigVerificationGasConsumer, - IBCKeeper: app.IBCKeeper, - EvmKeeper: app.EvmKeeper, - FeeMarketKeeper: app.FeeMarketKeeper, - MaxTxGasWanted: maxGasWanted, + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + SignModeHandler: txConfig.SignModeHandler(), + FeegrantKeeper: app.FeeGrantKeeper, + SigGasConsumer: ante.DefaultSigVerificationGasConsumer, + IBCKeeper: app.IBCKeeper, + EvmKeeper: app.EvmKeeper, + FeeMarketKeeper: app.FeeMarketKeeper, + MaxTxGasWanted: maxGasWanted, + ExtensionOptionChecker: ethermint.HasDynamicFeeExtensionOption, + TxFeeChecker: ante.NewDynamicFeeChecker(app.EvmKeeper), }) if err != nil { panic(err) diff --git a/app/simulation_test.go b/app/simulation_test.go index 8a9731fe..2f1d1b22 100644 --- a/app/simulation_test.go +++ b/app/simulation_test.go @@ -32,6 +32,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ibctransfertypes "github.com/cosmos/ibc-go/v5/modules/apps/transfer/types" ibchost "github.com/cosmos/ibc-go/v5/modules/core/24-host" + "github.com/evmos/ethermint/app/ante" evmenc "github.com/evmos/ethermint/encoding" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" @@ -62,6 +63,32 @@ func fauxMerkleModeOpt(bapp *baseapp.BaseApp) { bapp.SetFauxMerkleMode() } +// NewSimApp disable feemarket on native tx, otherwise the cosmos-sdk simulation tests will fail. +func NewSimApp(logger log.Logger, db dbm.DB) (*EthermintApp, error) { + encodingConfig := MakeEncodingConfig() + app := NewEthermintApp(logger, db, nil, false, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, encodingConfig, simapp.EmptyAppOptions{}, fauxMerkleModeOpt) + // disable feemarket on native tx + anteHandler, err := ante.NewAnteHandler(ante.HandlerOptions{ + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), + FeegrantKeeper: app.FeeGrantKeeper, + SigGasConsumer: ante.DefaultSigVerificationGasConsumer, + IBCKeeper: app.IBCKeeper, + EvmKeeper: app.EvmKeeper, + FeeMarketKeeper: app.FeeMarketKeeper, + MaxTxGasWanted: 0, + }) + if err != nil { + return nil, err + } + app.SetAnteHandler(anteHandler) + if err := app.LoadLatestVersion(); err != nil { + return nil, err + } + return app, nil +} + // interBlockCacheOpt returns a BaseApp option function that sets the persistent // inter-block write-through cache. func interBlockCacheOpt() func(*baseapp.BaseApp) { @@ -82,8 +109,9 @@ func TestFullAppSimulation(t *testing.T) { require.NoError(t, os.RemoveAll(dir)) }() - app := NewEthermintApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), simapp.EmptyAppOptions{}, fauxMerkleModeOpt) + app, err := NewSimApp(logger, db) require.Equal(t, appName, app.Name()) + require.NoError(t, err) // run randomized simulation _, simParams, simErr := simulation.SimulateFromSeed( @@ -121,8 +149,8 @@ func TestAppImportExport(t *testing.T) { require.NoError(t, db.Close()) require.NoError(t, os.RemoveAll(dir)) }() - - app := NewEthermintApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), simapp.EmptyAppOptions{}, fauxMerkleModeOpt) + app, err := NewSimApp(logger, db) + require.NoError(t, err) require.Equal(t, appName, app.Name()) // Run randomized simulation @@ -163,8 +191,9 @@ func TestAppImportExport(t *testing.T) { require.NoError(t, os.RemoveAll(newDir)) }() - newApp := NewEthermintApp(log.NewNopLogger(), newDB, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), simapp.EmptyAppOptions{}, fauxMerkleModeOpt) + newApp, err := NewSimApp(log.NewNopLogger(), newDB) require.Equal(t, appName, newApp.Name()) + require.NoError(t, err) var genesisState simapp.GenesisState err = json.Unmarshal(exported.AppState, &genesisState) @@ -236,8 +265,9 @@ func TestAppSimulationAfterImport(t *testing.T) { require.NoError(t, os.RemoveAll(dir)) }() - app := NewEthermintApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), simapp.EmptyAppOptions{}, fauxMerkleModeOpt) + app, err := NewSimApp(logger, db) require.Equal(t, appName, app.Name()) + require.NoError(t, err) // Run randomized simulation stopEarly, simParams, simErr := simulation.SimulateFromSeed( @@ -281,8 +311,9 @@ func TestAppSimulationAfterImport(t *testing.T) { require.NoError(t, os.RemoveAll(newDir)) }() - newApp := NewEthermintApp(log.NewNopLogger(), newDB, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), simapp.EmptyAppOptions{}, fauxMerkleModeOpt) + newApp, err := NewSimApp(log.NewNopLogger(), newDB) require.Equal(t, appName, newApp.Name()) + require.NoError(t, err) newApp.InitChain(abci.RequestInitChain{ ChainId: SimAppChainID, @@ -333,14 +364,15 @@ func TestAppStateDeterminism(t *testing.T) { } db := dbm.NewMemDB() - app := NewEthermintApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), simapp.EmptyAppOptions{}, interBlockCacheOpt()) + app, err := NewSimApp(logger, db) + require.NoError(t, err) fmt.Printf( "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n", config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, ) - _, _, err := simulation.SimulateFromSeed( + _, _, err = simulation.SimulateFromSeed( t, os.Stdout, app.BaseApp, diff --git a/docs/api/proto-docs.md b/docs/api/proto-docs.md index 6401c086..44602331 100644 --- a/docs/api/proto-docs.md +++ b/docs/api/proto-docs.md @@ -79,6 +79,9 @@ - [ethermint/types/v1/account.proto](#ethermint/types/v1/account.proto) - [EthAccount](#ethermint.types.v1.EthAccount) +- [ethermint/types/v1/dynamic_fee.proto](#ethermint/types/v1/dynamic_fee.proto) + - [ExtensionOptionDynamicFeeTx](#ethermint.types.v1.ExtensionOptionDynamicFeeTx) + - [ethermint/types/v1/web3.proto](#ethermint/types/v1/web3.proto) - [ExtensionOptionsWeb3Tx](#ethermint.types.v1.ExtensionOptionsWeb3Tx) @@ -1134,6 +1137,37 @@ authtypes.BaseAccount type. It is compatible with the auth AccountKeeper. + + + + + + + + + + + +
+ +## ethermint/types/v1/dynamic_fee.proto + + + + + +### ExtensionOptionDynamicFeeTx +ExtensionOptionDynamicFeeTx is an extension option that specify the maxPrioPrice for cosmos tx + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `max_priority_price` | [string](#string) | | the same as `max_priority_fee_per_gas` in eip-1559 spec | + + + + + diff --git a/proto/ethermint/types/v1/dynamic_fee.proto b/proto/ethermint/types/v1/dynamic_fee.proto new file mode 100644 index 00000000..ddd27e49 --- /dev/null +++ b/proto/ethermint/types/v1/dynamic_fee.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +package ethermint.types.v1; + +import "gogoproto/gogo.proto"; + +option go_package = "github.com/evmos/ethermint/types"; + +// ExtensionOptionDynamicFeeTx is an extension option that specify the maxPrioPrice for cosmos tx +message ExtensionOptionDynamicFeeTx { + // the same as `max_priority_fee_per_gas` in eip-1559 spec + string max_priority_price = 1 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int", + (gogoproto.nullable) = false + ]; +} diff --git a/rpc/backend/evm_backend_test.go b/rpc/backend/evm_backend_test.go index 82f40ddf..f17eed47 100644 --- a/rpc/backend/evm_backend_test.go +++ b/rpc/backend/evm_backend_test.go @@ -973,7 +973,6 @@ func (suite *BackendTestSuite) TestGetEthereumMsgsFromTendermintBlock() { } for _, tc := range testCases { suite.Run(fmt.Sprintf("Case %s", tc.name), func() { - suite.SetupTest() // reset test and queries msgs := suite.backend.GetEthereumMsgsFromTendermintBlock(tc.resBlock, tc.blockRes) diff --git a/tests/integration_tests/cosmoscli.py b/tests/integration_tests/cosmoscli.py new file mode 100644 index 00000000..de258881 --- /dev/null +++ b/tests/integration_tests/cosmoscli.py @@ -0,0 +1,828 @@ +import json +import tempfile + +import requests +from dateutil.parser import isoparse +from pystarport.utils import build_cli_args_safe, interact + +DEFAULT_GAS_PRICE = "5000000000000aphoton" + + +class ChainCommand: + def __init__(self, cmd): + self.cmd = cmd + + def __call__(self, cmd, *args, stdin=None, **kwargs): + "execute chain-maind" + args = " ".join(build_cli_args_safe(cmd, *args, **kwargs)) + return interact(f"{self.cmd} {args}", input=stdin) + + +class CosmosCLI: + "the apis to interact with wallet and blockchain" + + def __init__( + self, + data_dir, + node_rpc, + cmd, + ): + self.data_dir = data_dir + self._genesis = json.loads( + (self.data_dir / "config" / "genesis.json").read_text() + ) + self.chain_id = self._genesis["chain_id"] + self.node_rpc = node_rpc + self.raw = ChainCommand(cmd) + self.output = None + self.error = None + + @property + def node_rpc_http(self): + return "http" + self.node_rpc.removeprefix("tcp") + + def node_id(self): + "get tendermint node id" + output = self.raw("tendermint", "show-node-id", home=self.data_dir) + return output.decode().strip() + + def delete_account(self, name): + "delete wallet account in node's keyring" + return self.raw( + "keys", + "delete", + name, + "-y", + "--force", + home=self.data_dir, + output="json", + keyring_backend="test", + ) + + def create_account(self, name, mnemonic=None): + "create new keypair in node's keyring" + if mnemonic is None: + output = self.raw( + "keys", + "add", + name, + home=self.data_dir, + output="json", + keyring_backend="test", + ) + else: + output = self.raw( + "keys", + "add", + name, + "--recover", + home=self.data_dir, + output="json", + keyring_backend="test", + stdin=mnemonic.encode() + b"\n", + ) + return json.loads(output) + + def init(self, moniker): + "the node's config is already added" + return self.raw( + "init", + moniker, + chain_id=self.chain_id, + home=self.data_dir, + ) + + def validate_genesis(self): + return self.raw("validate-genesis", home=self.data_dir) + + def add_genesis_account(self, addr, coins, **kwargs): + return self.raw( + "add-genesis-account", + addr, + coins, + home=self.data_dir, + output="json", + **kwargs, + ) + + def gentx(self, name, coins, min_self_delegation=1, pubkey=None): + return self.raw( + "gentx", + name, + coins, + min_self_delegation=str(min_self_delegation), + home=self.data_dir, + chain_id=self.chain_id, + keyring_backend="test", + pubkey=pubkey, + ) + + def collect_gentxs(self, gentx_dir): + return self.raw("collect-gentxs", gentx_dir, home=self.data_dir) + + def status(self): + return json.loads(self.raw("status", node=self.node_rpc)) + + def block_height(self): + return int(self.status()["SyncInfo"]["latest_block_height"]) + + def block_time(self): + return isoparse(self.status()["SyncInfo"]["latest_block_time"]) + + def balances(self, addr): + return json.loads( + self.raw("query", "bank", "balances", addr, home=self.data_dir) + )["balances"] + + def balance(self, addr, denom="aphoton"): + denoms = {coin["denom"]: int(coin["amount"]) for coin in self.balances(addr)} + return denoms.get(denom, 0) + + def query_tx(self, tx_type, tx_value): + tx = self.raw( + "query", + "tx", + "--type", + tx_type, + tx_value, + home=self.data_dir, + chain_id=self.chain_id, + node=self.node_rpc, + ) + return json.loads(tx) + + def query_all_txs(self, addr): + txs = self.raw( + "query", + "txs-all", + addr, + home=self.data_dir, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + return json.loads(txs) + + def distribution_commission(self, addr): + coin = json.loads( + self.raw( + "query", + "distribution", + "commission", + addr, + output="json", + node=self.node_rpc, + ) + )["commission"][0] + return float(coin["amount"]) + + def distribution_community(self): + coin = json.loads( + self.raw( + "query", + "distribution", + "community-pool", + output="json", + node=self.node_rpc, + ) + )["pool"][0] + return float(coin["amount"]) + + def distribution_reward(self, delegator_addr): + coin = json.loads( + self.raw( + "query", + "distribution", + "rewards", + delegator_addr, + output="json", + node=self.node_rpc, + ) + )["total"][0] + return float(coin["amount"]) + + def address(self, name, bech="acc"): + output = self.raw( + "keys", + "show", + name, + "-a", + home=self.data_dir, + keyring_backend="test", + bech=bech, + ) + return output.strip().decode() + + def account(self, addr): + return json.loads( + self.raw( + "query", "auth", "account", addr, output="json", node=self.node_rpc + ) + ) + + def tx_search(self, events: str): + "/tx_search" + return json.loads( + self.raw("query", "txs", events=events, output="json", node=self.node_rpc) + ) + + def tx_search_rpc(self, events: str): + rsp = requests.get( + f"{self.node_rpc_http}/tx_search", + params={ + "query": f'"{events}"', + }, + ).json() + assert "error" not in rsp, rsp["error"] + return rsp["result"]["txs"] + + def tx(self, value, **kwargs): + "/tx" + default_kwargs = { + "home": self.data_dir, + } + return json.loads(self.raw("query", "tx", value, **(default_kwargs | kwargs))) + + def total_supply(self): + return json.loads( + self.raw("query", "bank", "total", output="json", node=self.node_rpc) + ) + + def validator(self, addr): + return json.loads( + self.raw( + "query", + "staking", + "validator", + addr, + output="json", + node=self.node_rpc, + ) + ) + + def validators(self): + return json.loads( + self.raw( + "query", "staking", "validators", output="json", node=self.node_rpc + ) + )["validators"] + + def staking_params(self): + return json.loads( + self.raw("query", "staking", "params", output="json", node=self.node_rpc) + ) + + def staking_pool(self, bonded=True): + return int( + json.loads( + self.raw("query", "staking", "pool", output="json", node=self.node_rpc) + )["bonded_tokens" if bonded else "not_bonded_tokens"] + ) + + def transfer(self, from_, to, coins, generate_only=False, **kwargs): + kwargs.setdefault("gas_prices", DEFAULT_GAS_PRICE) + return json.loads( + self.raw( + "tx", + "bank", + "send", + from_, + to, + coins, + "-y", + "--generate-only" if generate_only else None, + home=self.data_dir, + **kwargs, + ) + ) + + def get_delegated_amount(self, which_addr): + return json.loads( + self.raw( + "query", + "staking", + "delegations", + which_addr, + home=self.data_dir, + chain_id=self.chain_id, + node=self.node_rpc, + output="json", + ) + ) + + def delegate_amount(self, to_addr, amount, from_addr, gas_price=None): + if gas_price is None: + return json.loads( + self.raw( + "tx", + "staking", + "delegate", + to_addr, + amount, + "-y", + home=self.data_dir, + from_=from_addr, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + ) + else: + return json.loads( + self.raw( + "tx", + "staking", + "delegate", + to_addr, + amount, + "-y", + home=self.data_dir, + from_=from_addr, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + gas_prices=gas_price, + ) + ) + + # to_addr: croclcl1... , from_addr: cro1... + def unbond_amount(self, to_addr, amount, from_addr): + return json.loads( + self.raw( + "tx", + "staking", + "unbond", + to_addr, + amount, + "-y", + home=self.data_dir, + from_=from_addr, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + ) + + # to_validator_addr: crocncl1... , from_from_validator_addraddr: crocl1... + def redelegate_amount( + self, to_validator_addr, from_validator_addr, amount, from_addr + ): + return json.loads( + self.raw( + "tx", + "staking", + "redelegate", + from_validator_addr, + to_validator_addr, + amount, + "-y", + home=self.data_dir, + from_=from_addr, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + ) + + # from_delegator can be account name or address + def withdraw_all_rewards(self, from_delegator): + return json.loads( + self.raw( + "tx", + "distribution", + "withdraw-all-rewards", + "-y", + from_=from_delegator, + home=self.data_dir, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + ) + + def make_multisig(self, name, signer1, signer2): + self.raw( + "keys", + "add", + name, + multisig=f"{signer1},{signer2}", + multisig_threshold="2", + home=self.data_dir, + keyring_backend="test", + ) + + def sign_multisig_tx(self, tx_file, multi_addr, signer_name): + return json.loads( + self.raw( + "tx", + "sign", + tx_file, + from_=signer_name, + multisig=multi_addr, + home=self.data_dir, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + ) + + def sign_batch_multisig_tx( + self, tx_file, multi_addr, signer_name, account_number, sequence_number + ): + r = self.raw( + "tx", + "sign-batch", + "--offline", + tx_file, + account_number=account_number, + sequence=sequence_number, + from_=signer_name, + multisig=multi_addr, + home=self.data_dir, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + return r.decode("utf-8") + + def encode_signed_tx(self, signed_tx): + return self.raw( + "tx", + "encode", + signed_tx, + ) + + def sign_tx(self, tx_file, signer): + return json.loads( + self.raw( + "tx", + "sign", + tx_file, + from_=signer, + home=self.data_dir, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + ) + + def sign_tx_json(self, tx, signer, max_priority_price=None): + if max_priority_price is not None: + tx["body"]["extension_options"].append( + { + "@type": "/ethermint.types.v1.ExtensionOptionDynamicFeeTx", + "max_priority_price": str(max_priority_price), + } + ) + with tempfile.NamedTemporaryFile("w") as fp: + json.dump(tx, fp) + fp.flush() + return self.sign_tx(fp.name, signer) + + def combine_multisig_tx(self, tx_file, multi_name, signer1_file, signer2_file): + return json.loads( + self.raw( + "tx", + "multisign", + tx_file, + multi_name, + signer1_file, + signer2_file, + home=self.data_dir, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + ) + + def combine_batch_multisig_tx( + self, tx_file, multi_name, signer1_file, signer2_file + ): + r = self.raw( + "tx", + "multisign-batch", + tx_file, + multi_name, + signer1_file, + signer2_file, + home=self.data_dir, + keyring_backend="test", + chain_id=self.chain_id, + node=self.node_rpc, + ) + return r.decode("utf-8") + + def broadcast_tx(self, tx_file, **kwargs): + kwargs.setdefault("broadcast_mode", "block") + kwargs.setdefault("output", "json") + return json.loads( + self.raw("tx", "broadcast", tx_file, node=self.node_rpc, **kwargs) + ) + + def broadcast_tx_json(self, tx, **kwargs): + with tempfile.NamedTemporaryFile("w") as fp: + json.dump(tx, fp) + fp.flush() + return self.broadcast_tx(fp.name, **kwargs) + + def unjail(self, addr): + return json.loads( + self.raw( + "tx", + "slashing", + "unjail", + "-y", + from_=addr, + home=self.data_dir, + node=self.node_rpc, + keyring_backend="test", + chain_id=self.chain_id, + ) + ) + + def create_validator( + self, + amount, + moniker=None, + commission_max_change_rate="0.01", + commission_rate="0.1", + commission_max_rate="0.2", + min_self_delegation="1", + identity="", + website="", + security_contact="", + details="", + ): + """MsgCreateValidator + create the node with create_node before call this""" + pubkey = ( + "'" + + ( + self.raw( + "tendermint", + "show-validator", + home=self.data_dir, + ) + .strip() + .decode() + ) + + "'" + ) + return json.loads( + self.raw( + "tx", + "staking", + "create-validator", + "-y", + from_=self.address("validator"), + amount=amount, + pubkey=pubkey, + min_self_delegation=min_self_delegation, + # commision + commission_rate=commission_rate, + commission_max_rate=commission_max_rate, + commission_max_change_rate=commission_max_change_rate, + # description + moniker=moniker, + identity=identity, + website=website, + security_contact=security_contact, + details=details, + # basic + home=self.data_dir, + node=self.node_rpc, + keyring_backend="test", + chain_id=self.chain_id, + ) + ) + + def edit_validator( + self, + commission_rate=None, + moniker=None, + identity=None, + website=None, + security_contact=None, + details=None, + ): + """MsgEditValidator""" + options = dict( + commission_rate=commission_rate, + # description + moniker=moniker, + identity=identity, + website=website, + security_contact=security_contact, + details=details, + ) + return json.loads( + self.raw( + "tx", + "staking", + "edit-validator", + "-y", + from_=self.address("validator"), + home=self.data_dir, + node=self.node_rpc, + keyring_backend="test", + chain_id=self.chain_id, + **{k: v for k, v in options.items() if v is not None}, + ) + ) + + def gov_propose(self, proposer, kind, proposal, **kwargs): + kwargs.setdefault("gas_prices", DEFAULT_GAS_PRICE) + if kind == "software-upgrade": + return json.loads( + self.raw( + "tx", + "gov", + "submit-proposal", + kind, + proposal["name"], + "-y", + from_=proposer, + # content + title=proposal.get("title"), + description=proposal.get("description"), + upgrade_height=proposal.get("upgrade-height"), + upgrade_time=proposal.get("upgrade-time"), + upgrade_info=proposal.get("upgrade-info"), + deposit=proposal.get("deposit"), + # basic + home=self.data_dir, + **kwargs, + ) + ) + elif kind == "cancel-software-upgrade": + return json.loads( + self.raw( + "tx", + "gov", + "submit-proposal", + kind, + "-y", + from_=proposer, + # content + title=proposal.get("title"), + description=proposal.get("description"), + deposit=proposal.get("deposit"), + # basic + home=self.data_dir, + **kwargs, + ) + ) + else: + with tempfile.NamedTemporaryFile("w") as fp: + json.dump(proposal, fp) + fp.flush() + return json.loads( + self.raw( + "tx", + "gov", + "submit-proposal", + kind, + fp.name, + "-y", + from_=proposer, + # basic + home=self.data_dir, + **kwargs, + ) + ) + + def gov_vote(self, voter, proposal_id, option, **kwargs): + kwargs.setdefault("gas_prices", DEFAULT_GAS_PRICE) + return json.loads( + self.raw( + "tx", + "gov", + "vote", + proposal_id, + option, + "-y", + from_=voter, + home=self.data_dir, + **kwargs, + ) + ) + + def gov_deposit(self, depositor, proposal_id, amount): + return json.loads( + self.raw( + "tx", + "gov", + "deposit", + proposal_id, + amount, + "-y", + from_=depositor, + home=self.data_dir, + node=self.node_rpc, + keyring_backend="test", + chain_id=self.chain_id, + ) + ) + + def query_proposals(self, depositor=None, limit=None, status=None, voter=None): + return json.loads( + self.raw( + "query", + "gov", + "proposals", + depositor=depositor, + count_total=limit, + status=status, + voter=voter, + output="json", + node=self.node_rpc, + ) + ) + + def query_proposal(self, proposal_id): + return json.loads( + self.raw( + "query", + "gov", + "proposal", + proposal_id, + output="json", + node=self.node_rpc, + ) + ) + + def query_tally(self, proposal_id): + return json.loads( + self.raw( + "query", + "gov", + "tally", + proposal_id, + output="json", + node=self.node_rpc, + ) + ) + + def ibc_transfer( + self, + from_, + to, + amount, + channel, # src channel + target_version, # chain version number of target chain + i=0, + ): + return json.loads( + self.raw( + "tx", + "ibc-transfer", + "transfer", + "transfer", # src port + channel, + to, + amount, + "-y", + # FIXME https://github.com/cosmos/cosmos-sdk/issues/8059 + "--absolute-timeouts", + from_=from_, + home=self.data_dir, + node=self.node_rpc, + keyring_backend="test", + chain_id=self.chain_id, + packet_timeout_height=f"{target_version}-10000000000", + packet_timeout_timestamp=0, + ) + ) + + def export(self): + return self.raw("export", home=self.data_dir) + + def unsaferesetall(self): + return self.raw("unsafe-reset-all") + + def build_evm_tx(self, raw_tx: str, **kwargs): + return json.loads( + self.raw( + "tx", + "evm", + "raw", + raw_tx, + "-y", + "--generate-only", + home=self.data_dir, + **kwargs, + ) + ) + + def query_base_fee(self, **kwargs): + default_kwargs = {"home": self.data_dir} + return int( + json.loads( + self.raw( + "q", + "feemarket", + "base-fee", + **(default_kwargs | kwargs), + ) + )["base_fee"] + ) diff --git a/tests/integration_tests/network.py b/tests/integration_tests/network.py index a843a340..5a413db2 100644 --- a/tests/integration_tests/network.py +++ b/tests/integration_tests/network.py @@ -8,6 +8,7 @@ import web3 from pystarport import ports from web3.middleware import geth_poa_middleware +from .cosmoscli import CosmosCLI from .utils import wait_for_port @@ -53,6 +54,9 @@ class Ethermint: self._w3 = None self._use_websockets = use + def cosmos_cli(self, i=0): + return CosmosCLI(self.base_dir / f"node{i}", self.node_rpc(i), "ethermintd") + class Geth: def __init__(self, w3): diff --git a/tests/integration_tests/test_priority.py b/tests/integration_tests/test_priority.py index f51eb4ea..46d4a449 100644 --- a/tests/integration_tests/test_priority.py +++ b/tests/integration_tests/test_priority.py @@ -1,5 +1,7 @@ +import sys + from .network import Ethermint -from .utils import KEYS, sign_transaction +from .utils import ADDRS, KEYS, eth_to_bech32, sign_transaction, wait_for_new_blocks PRIORITY_REDUCTION = 1000000 @@ -101,14 +103,95 @@ def test_priority(ethermint: Ethermint): # the later txs should be included earlier because of higher priority # FIXME there's some non-deterministics due to mempool logic - assert all(included_earlier(r2, r1) for r1, r2 in zip(receipts, receipts[1:])) + tx_indexes = [(r.blockNumber, r.transactionIndex) for r in receipts] + print(tx_indexes) + # the first sent tx are included later, because of lower priority + assert all(i1 > i2 for i1, i2 in zip(tx_indexes, tx_indexes[1:])) def included_earlier(receipt1, receipt2): - "returns true if receipt1 is earlier than receipt2" - if receipt1.blockNumber < receipt2.blockNumber: - return True - elif receipt1.blockNumber == receipt2.blockNumber: - return receipt1.transactionIndex < receipt2.transactionIndex - else: - return False + "returns true if receipt1 is included earlier than receipt2" + return (receipt1.blockNumber, receipt1.transactionIndex) < ( + receipt2.blockNumber, + receipt2.transactionIndex, + ) + + +def test_native_tx_priority(ethermint: Ethermint): + cli = ethermint.cosmos_cli() + base_fee = cli.query_base_fee() + print("base_fee", base_fee) + test_cases = [ + { + "from": eth_to_bech32(ADDRS["community"]), + "to": eth_to_bech32(ADDRS["validator"]), + "amount": "1000aphoton", + "gas_prices": f"{base_fee + PRIORITY_REDUCTION * 6}aphoton", + "max_priority_price": 0, + }, + { + "from": eth_to_bech32(ADDRS["signer1"]), + "to": eth_to_bech32(ADDRS["signer2"]), + "amount": "1000aphoton", + "gas_prices": f"{base_fee + PRIORITY_REDUCTION * 6}aphoton", + "max_priority_price": PRIORITY_REDUCTION * 2, + }, + { + "from": eth_to_bech32(ADDRS["signer2"]), + "to": eth_to_bech32(ADDRS["signer1"]), + "amount": "1000aphoton", + "gas_prices": f"{base_fee + PRIORITY_REDUCTION * 4}aphoton", + "max_priority_price": PRIORITY_REDUCTION * 4, + }, + { + "from": eth_to_bech32(ADDRS["validator"]), + "to": eth_to_bech32(ADDRS["community"]), + "amount": "1000aphoton", + "gas_prices": f"{base_fee + PRIORITY_REDUCTION * 6}aphoton", + "max_priority_price": None, # no extension, maximum tipFeeCap + }, + ] + txs = [] + expect_priorities = [] + for tc in test_cases: + tx = cli.transfer( + tc["from"], + tc["to"], + tc["amount"], + gas_prices=tc["gas_prices"], + generate_only=True, + ) + txs.append( + cli.sign_tx_json( + tx, tc["from"], max_priority_price=tc.get("max_priority_price") + ) + ) + gas_price = int(tc["gas_prices"].removesuffix("aphoton")) + expect_priorities.append( + min( + get_max_priority_price(tc.get("max_priority_price")), + gas_price - base_fee, + ) + // PRIORITY_REDUCTION + ) + assert expect_priorities == [0, 2, 4, 6] + + txhashes = [] + for tx in txs: + rsp = cli.broadcast_tx_json(tx, broadcast_mode="sync") + assert rsp["code"] == 0, rsp["raw_log"] + txhashes.append(rsp["txhash"]) + + print("wait for two new blocks, so the sent txs are all included") + wait_for_new_blocks(cli, 2) + + tx_results = [cli.tx_search_rpc(f"tx.hash='{txhash}'")[0] for txhash in txhashes] + tx_indexes = [(int(r["height"]), r["index"]) for r in tx_results] + print(tx_indexes) + # the first sent tx are included later, because of lower priority + assert all(i1 > i2 for i1, i2 in zip(tx_indexes, tx_indexes[1:])) + + +def get_max_priority_price(max_priority_price): + "default to max int64 if None" + return max_priority_price if max_priority_price is not None else sys.maxsize diff --git a/tests/integration_tests/utils.py b/tests/integration_tests/utils.py index ed15b35c..a2ea48a1 100644 --- a/tests/integration_tests/utils.py +++ b/tests/integration_tests/utils.py @@ -4,8 +4,10 @@ import socket import time from pathlib import Path +import bech32 from dotenv import load_dotenv from eth_account import Account +from hexbytes import HexBytes from web3._utils.transactions import fill_nonce, fill_transaction_defaults load_dotenv(Path(__file__).parent.parent.parent / "scripts/.env") @@ -64,6 +66,15 @@ def w3_wait_for_new_blocks(w3, n): break +def wait_for_new_blocks(cli, n): + begin_height = int((cli.status())["SyncInfo"]["latest_block_height"]) + while True: + time.sleep(0.5) + cur_height = int((cli.status())["SyncInfo"]["latest_block_height"]) + if cur_height - begin_height >= n: + break + + def deploy_contract(w3, jsonfile, args=(), key=KEYS["validator"]): """ deploy contract and return the deployed contract instance @@ -95,3 +106,8 @@ def send_transaction(w3, tx, key=KEYS["validator"]): signed = sign_transaction(w3, tx, key) txhash = w3.eth.send_raw_transaction(signed.rawTransaction) return w3.eth.wait_for_transaction_receipt(txhash) + + +def eth_to_bech32(addr, prefix=ETHERMINT_ADDRESS_PREFIX): + bz = bech32.convertbits(HexBytes(addr), 8, 5) + return bech32.bech32_encode(prefix, bz) diff --git a/types/codec.go b/types/codec.go index c9840fdf..28771634 100644 --- a/types/codec.go +++ b/types/codec.go @@ -20,5 +20,6 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { registry.RegisterImplementations( (*tx.TxExtensionOptionI)(nil), &ExtensionOptionsWeb3Tx{}, + &ExtensionOptionDynamicFeeTx{}, ) } diff --git a/types/dynamic_fee.go b/types/dynamic_fee.go new file mode 100644 index 00000000..feab55fe --- /dev/null +++ b/types/dynamic_fee.go @@ -0,0 +1,11 @@ +package types + +import ( + codectypes "github.com/cosmos/cosmos-sdk/codec/types" +) + +// HasDynamicFeeExtensionOption returns true if the tx implements the `ExtensionOptionDynamicFeeTx` extension option. +func HasDynamicFeeExtensionOption(any *codectypes.Any) bool { + _, ok := any.GetCachedValue().(*ExtensionOptionDynamicFeeTx) + return ok +} diff --git a/types/dynamic_fee.pb.go b/types/dynamic_fee.pb.go new file mode 100644 index 00000000..e1903dee --- /dev/null +++ b/types/dynamic_fee.pb.go @@ -0,0 +1,321 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: ethermint/types/v1/dynamic_fee.proto + +package types + +import ( + fmt "fmt" + github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// ExtensionOptionDynamicFeeTx is an extension option that specify the maxPrioPrice for cosmos tx +type ExtensionOptionDynamicFeeTx struct { + // the same as `max_priority_fee_per_gas` in eip-1559 spec + MaxPriorityPrice github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,1,opt,name=max_priority_price,json=maxPriorityPrice,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"max_priority_price"` +} + +func (m *ExtensionOptionDynamicFeeTx) Reset() { *m = ExtensionOptionDynamicFeeTx{} } +func (m *ExtensionOptionDynamicFeeTx) String() string { return proto.CompactTextString(m) } +func (*ExtensionOptionDynamicFeeTx) ProtoMessage() {} +func (*ExtensionOptionDynamicFeeTx) Descriptor() ([]byte, []int) { + return fileDescriptor_9d7cf05c9992c480, []int{0} +} +func (m *ExtensionOptionDynamicFeeTx) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ExtensionOptionDynamicFeeTx) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ExtensionOptionDynamicFeeTx.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ExtensionOptionDynamicFeeTx) XXX_Merge(src proto.Message) { + xxx_messageInfo_ExtensionOptionDynamicFeeTx.Merge(m, src) +} +func (m *ExtensionOptionDynamicFeeTx) XXX_Size() int { + return m.Size() +} +func (m *ExtensionOptionDynamicFeeTx) XXX_DiscardUnknown() { + xxx_messageInfo_ExtensionOptionDynamicFeeTx.DiscardUnknown(m) +} + +var xxx_messageInfo_ExtensionOptionDynamicFeeTx proto.InternalMessageInfo + +func init() { + proto.RegisterType((*ExtensionOptionDynamicFeeTx)(nil), "ethermint.types.v1.ExtensionOptionDynamicFeeTx") +} + +func init() { + proto.RegisterFile("ethermint/types/v1/dynamic_fee.proto", fileDescriptor_9d7cf05c9992c480) +} + +var fileDescriptor_9d7cf05c9992c480 = []byte{ + // 232 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x49, 0x2d, 0xc9, 0x48, + 0x2d, 0xca, 0xcd, 0xcc, 0x2b, 0xd1, 0x2f, 0xa9, 0x2c, 0x48, 0x2d, 0xd6, 0x2f, 0x33, 0xd4, 0x4f, + 0xa9, 0xcc, 0x4b, 0xcc, 0xcd, 0x4c, 0x8e, 0x4f, 0x4b, 0x4d, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, + 0x17, 0x12, 0x82, 0xab, 0xd2, 0x03, 0xab, 0xd2, 0x2b, 0x33, 0x94, 0x12, 0x49, 0xcf, 0x4f, 0xcf, + 0x07, 0x4b, 0xeb, 0x83, 0x58, 0x10, 0x95, 0x4a, 0xd5, 0x5c, 0xd2, 0xae, 0x15, 0x25, 0xa9, 0x79, + 0xc5, 0x99, 0xf9, 0x79, 0xfe, 0x05, 0x25, 0x99, 0xf9, 0x79, 0x2e, 0x10, 0xd3, 0xdc, 0x52, 0x53, + 0x43, 0x2a, 0x84, 0x62, 0xb8, 0x84, 0x72, 0x13, 0x2b, 0xe2, 0x0b, 0x8a, 0x32, 0xf3, 0x8b, 0x32, + 0x4b, 0x2a, 0x41, 0x8c, 0xe4, 0x54, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x4e, 0x27, 0xbd, 0x13, 0xf7, + 0xe4, 0x19, 0x6e, 0xdd, 0x93, 0x57, 0x4b, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, + 0xd5, 0x4f, 0xce, 0x2f, 0xce, 0xcd, 0x2f, 0x86, 0x52, 0xba, 0xc5, 0x29, 0xd9, 0x10, 0x57, 0xea, + 0x79, 0xe6, 0x95, 0x04, 0x09, 0xe4, 0x26, 0x56, 0x04, 0x40, 0x0d, 0x0a, 0x00, 0x99, 0xe3, 0x64, + 0x75, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, 0x1e, 0xc9, 0x31, 0x4e, 0x78, 0x2c, + 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7, 0x72, 0x0c, 0x51, 0x0a, 0x48, 0x66, 0xa6, 0x96, + 0x81, 0x8c, 0x44, 0xf3, 0x77, 0x12, 0x1b, 0xd8, 0xfd, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x5c, 0xa9, 0x04, 0x48, 0x11, 0x01, 0x00, 0x00, +} + +func (m *ExtensionOptionDynamicFeeTx) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ExtensionOptionDynamicFeeTx) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ExtensionOptionDynamicFeeTx) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.MaxPriorityPrice.Size() + i -= size + if _, err := m.MaxPriorityPrice.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintDynamicFee(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func encodeVarintDynamicFee(dAtA []byte, offset int, v uint64) int { + offset -= sovDynamicFee(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *ExtensionOptionDynamicFeeTx) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.MaxPriorityPrice.Size() + n += 1 + l + sovDynamicFee(uint64(l)) + return n +} + +func sovDynamicFee(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozDynamicFee(x uint64) (n int) { + return sovDynamicFee(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *ExtensionOptionDynamicFeeTx) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowDynamicFee + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ExtensionOptionDynamicFeeTx: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ExtensionOptionDynamicFeeTx: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxPriorityPrice", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowDynamicFee + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthDynamicFee + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthDynamicFee + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.MaxPriorityPrice.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipDynamicFee(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthDynamicFee + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipDynamicFee(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowDynamicFee + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowDynamicFee + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowDynamicFee + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthDynamicFee + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupDynamicFee + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthDynamicFee + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthDynamicFee = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowDynamicFee = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupDynamicFee = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/evm/keeper/utils.go b/x/evm/keeper/utils.go index 4198a95b..2b7465b3 100644 --- a/x/evm/keeper/utils.go +++ b/x/evm/keeper/utils.go @@ -15,11 +15,6 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" ) -// DefaultPriorityReduction is the default amount of price values required for 1 unit of priority. -// Because priority is `int64` while price is `big.Int`, it's necessary to scale down the range to keep it more pratical. -// The default value is the same as the `sdk.DefaultPowerReduction`. -var DefaultPriorityReduction = sdk.DefaultPowerReduction - // DeductTxCostsFromUserBalance it calculates the tx costs and deducts the fees // returns (effectiveFee, priority, error) func (k Keeper) DeductTxCostsFromUserBalance( @@ -91,7 +86,7 @@ func (k Keeper) DeductTxCostsFromUserBalance( if baseFee != nil { tipPrice = new(big.Int).Sub(tipPrice, baseFee) } - priorityBig := new(big.Int).Quo(tipPrice, DefaultPriorityReduction.BigInt()) + priorityBig := new(big.Int).Quo(tipPrice, evmtypes.DefaultPriorityReduction.BigInt()) if !priorityBig.IsInt64() { priority = math.MaxInt64 } else { diff --git a/x/evm/types/dynamic_fee_tx.go b/x/evm/types/dynamic_fee_tx.go index e094e92c..a2b04bf1 100644 --- a/x/evm/types/dynamic_fee_tx.go +++ b/x/evm/types/dynamic_fee_tx.go @@ -7,7 +7,6 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/evmos/ethermint/types" @@ -267,7 +266,7 @@ func (tx DynamicFeeTx) Cost() *big.Int { // EffectiveGasPrice returns the effective gas price func (tx *DynamicFeeTx) EffectiveGasPrice(baseFee *big.Int) *big.Int { - return math.BigMin(new(big.Int).Add(tx.GasTipCap.BigInt(), baseFee), tx.GasFeeCap.BigInt()) + return EffectiveGasPrice(baseFee, tx.GasFeeCap.BigInt(), tx.GasTipCap.BigInt()) } // EffectiveFee returns effective_gasprice * gaslimit. diff --git a/x/evm/types/evm.pb.go b/x/evm/types/evm.pb.go index 2236d794..42580f92 100644 --- a/x/evm/types/evm.pb.go +++ b/x/evm/types/evm.pb.go @@ -117,7 +117,7 @@ func (m *Params) GetAllowUnprotectedTxs() bool { return false } -// ChainConfig defines the Ethereum ChainConfig parameters using *sdkmath.Int values +// ChainConfig defines the Ethereum ChainConfig parameters using *sdk.Int values // instead of *big.Int. type ChainConfig struct { // Homestead switch block (nil no fork, 0 = already homestead) diff --git a/x/evm/types/utils.go b/x/evm/types/utils.go index bbc72f36..b8ffd782 100644 --- a/x/evm/types/utils.go +++ b/x/evm/types/utils.go @@ -2,6 +2,7 @@ package types import ( "fmt" + "math/big" "github.com/gogo/protobuf/proto" @@ -9,9 +10,15 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" ) +// DefaultPriorityReduction is the default amount of price values required for 1 unit of priority. +// Because priority is `int64` while price is `big.Int`, it's necessary to scale down the range to keep it more pratical. +// The default value is the same as the `sdk.DefaultPowerReduction`. +var DefaultPriorityReduction = sdk.DefaultPowerReduction + var EmptyCodeHash = crypto.Keccak256(nil) // DecodeTxResponse decodes an protobuf-encoded byte slice into TxResponse @@ -88,3 +95,9 @@ func BinSearch(lo, hi uint64, executable func(uint64) (bool, *MsgEthereumTxRespo } return hi, nil } + +// EffectiveGasPrice compute the effective gas price based on eip-1159 rules +// `effectiveGasPrice = min(baseFee + tipCap, feeCap)` +func EffectiveGasPrice(baseFee *big.Int, feeCap *big.Int, tipCap *big.Int) *big.Int { + return math.BigMin(new(big.Int).Add(tipCap, baseFee), feeCap) +} diff --git a/x/feemarket/keeper/integration_test.go b/x/feemarket/keeper/integration_test.go index 41b47991..db167b29 100644 --- a/x/feemarket/keeper/integration_test.go +++ b/x/feemarket/keeper/integration_test.go @@ -126,7 +126,7 @@ var _ = Describe("Feemarket", func() { Context("with MinGasPrices (feemarket param) < min-gas-prices (local)", func() { BeforeEach(func() { - privKey, msg = setupTestWithContext("5", sdk.NewDec(3), sdk.ZeroInt()) + privKey, msg = setupTestWithContext("5", sdk.NewDec(3), sdk.NewInt(5)) }) Context("during CheckTx", func() { It("should reject transactions with gasPrice < MinGasPrices", func() { @@ -139,7 +139,7 @@ var _ = Describe("Feemarket", func() { ).To(BeTrue(), res.GetLog()) }) - It("should reject transactions with MinGasPrices < gasPrice < min-gas-prices", func() { + It("should reject transactions with MinGasPrices < gasPrice < baseFee", func() { gasPrice := sdkmath.NewInt(4) res := checkTx(privKey, &gasPrice, &msg) Expect(res.IsOK()).To(Equal(false), "transaction should have failed") @@ -149,7 +149,7 @@ var _ = Describe("Feemarket", func() { ).To(BeTrue(), res.GetLog()) }) - It("should accept transactions with gasPrice > min-gas-prices", func() { + It("should accept transactions with gasPrice >= baseFee", func() { gasPrice := sdkmath.NewInt(5) res := checkTx(privKey, &gasPrice, &msg) Expect(res.IsOK()).To(Equal(true), "transaction should have succeeded", res.GetLog()) @@ -167,13 +167,16 @@ var _ = Describe("Feemarket", func() { ).To(BeTrue(), res.GetLog()) }) - It("should accept transactions with MinGasPrices < gasPrice < than min-gas-prices", func() { + It("should reject transactions with MinGasPrices < gasPrice < baseFee", func() { gasPrice := sdkmath.NewInt(4) - res := deliverTx(privKey, &gasPrice, &msg) - Expect(res.IsOK()).To(Equal(true), "transaction should have succeeded", res.GetLog()) + res := checkTx(privKey, &gasPrice, &msg) + Expect(res.IsOK()).To(Equal(false), "transaction should have failed") + Expect( + strings.Contains(res.GetLog(), + "insufficient fee"), + ).To(BeTrue(), res.GetLog()) }) - - It("should accept transactions with gasPrice >= min-gas-prices", func() { + It("should accept transactions with gasPrice >= baseFee", func() { gasPrice := sdkmath.NewInt(5) res := deliverTx(privKey, &gasPrice, &msg) Expect(res.IsOK()).To(Equal(true), "transaction should have succeeded", res.GetLog())