diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index e410522ac8bf..857573663e6b 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -63,13 +63,16 @@ type SimulatedBackend struct { database ethdb.Database // In memory database to store our testing data blockchain *core.BlockChain // Ethereum blockchain to handle the consensus - mu sync.Mutex - pendingBlock *types.Block // Currently pending block that will be imported on request - pendingState *state.StateDB // Currently pending state that will be the active on request + mu sync.Mutex + stuckTransactions types.Transactions // holds onto all txes that don't go into the pending block due to low gas + pendingBlock *types.Block // Currently pending block that will be imported on request + pendingState *state.StateDB // Currently pending state that will be the active on request events *filters.EventSystem // Event system for filtering log events live config *params.ChainConfig + + marketGasPrice *big.Int // minimum gas price it needs to have the block mined in simulated blockchain } // NewSimulatedBackendWithDatabase creates a new binding backend based on the given database @@ -109,12 +112,28 @@ func (b *SimulatedBackend) Commit() { b.mu.Lock() defer b.mu.Unlock() - if _, err := b.blockchain.InsertChain([]*types.Block{b.pendingBlock}); err != nil { + // Get the last block + parentBlock := b.blockchain.GetBlockByHash(b.pendingBlock.ParentHash()) + var remainingTx types.Transactions + + blocks, _ := core.GenerateChain(b.config, parentBlock, ethash.NewFaker(), b.database, 1, + func(number int, block *core.BlockGen) { + for _, tx := range append(b.pendingBlock.Transactions(), b.stuckTransactions...) { + if b.marketGasPrice == nil || b.marketGasPrice.Cmp(tx.GasPrice()) <= 0 { + block.AddTxWithChain(b.blockchain, tx) + } else { + remainingTx = append(remainingTx, tx) + } + } + }) + + if _, err := b.blockchain.InsertChain([]*types.Block{blocks[0]}); err != nil { panic(err) // This cannot happen unless the simulator is wrong, fail in that case } // Using the last inserted block here makes it possible to build on a side // chain after a fork. - b.rollback(b.pendingBlock) + b.stuckTransactions = remainingTx + b.rollback(blocks[0]) } // Rollback aborts all pending transactions, reverting to the last committed state. @@ -127,7 +146,6 @@ func (b *SimulatedBackend) Rollback() { func (b *SimulatedBackend) rollback(parent *types.Block) { blocks, _ := core.GenerateChain(b.config, parent, ethash.NewFaker(), b.database, 1, func(int, *core.BlockGen) {}) - b.pendingBlock = blocks[0] b.pendingState, _ = state.New(b.pendingBlock.Root(), b.blockchain.StateCache(), nil) } @@ -210,6 +228,15 @@ func (b *SimulatedBackend) NonceAt(ctx context.Context, contract common.Address, return stateDB.GetNonce(contract), nil } +// SetMarketGasPrice sets the simulated blockchain's market gas price, which is +// the minimum price point for the transaction to be committed to the simulated chain. +func (b *SimulatedBackend) SetMarketGasPrice(gasPrice int64) { + b.mu.Lock() + defer b.mu.Unlock() + + b.marketGasPrice = big.NewInt(gasPrice) +} + // StorageAt returns the value of key in the storage of an account in the blockchain. func (b *SimulatedBackend) StorageAt(ctx context.Context, contract common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { b.mu.Lock() @@ -653,17 +680,30 @@ func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transa if tx.Nonce() != nonce { panic(fmt.Errorf("invalid transaction nonce: got %d, want %d", tx.Nonce(), nonce)) } + var remainingTxes types.Transactions // Include tx in chain blocks, _ := core.GenerateChain(b.config, block, ethash.NewFaker(), b.database, 1, func(number int, block *core.BlockGen) { for _, tx := range b.pendingBlock.Transactions() { + // if market gas price is not set or tx gas price is lower than the set market gas price + if b.marketGasPrice == nil || b.marketGasPrice.Cmp(tx.GasPrice()) <= 0 { + block.AddTxWithChain(b.blockchain, tx) + } else { + remainingTxes = append(remainingTxes, tx) + } + } + if b.marketGasPrice == nil || b.marketGasPrice.Cmp(tx.GasPrice()) <= 0 { block.AddTxWithChain(b.blockchain, tx) + } else { + remainingTxes = append(remainingTxes, tx) } - block.AddTxWithChain(b.blockchain, tx) }) + stateDB, _ := b.blockchain.State() b.pendingBlock = blocks[0] b.pendingState, _ = state.New(b.pendingBlock.Root(), stateDB.Database(), nil) + b.stuckTransactions = remainingTxes + return nil } diff --git a/accounts/abi/bind/backends/simulated_test.go b/accounts/abi/bind/backends/simulated_test.go index 613267810744..78dfa823f6e1 100644 --- a/accounts/abi/bind/backends/simulated_test.go +++ b/accounts/abi/bind/backends/simulated_test.go @@ -27,6 +27,9 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -1336,3 +1339,201 @@ func TestForkResendTx(t *testing.T) { t.Errorf("TX included in wrong block: %d", h) } } + +// TestLowGasTxNotBeingMined checks that the lower gas fee tx with same nonce does not get mined +// Steps: +// 1. Send a tx lower than the set market gas price +// 2. Commit to chain, check nonce stays the same +// 3. Send a tx meets the market gas price with same nonce. +// 4. Commit to chain +// 5. Check tx2 is not in pending state and nonce has increased +func TestLowGasTxNotBeingMined(t *testing.T) { + testAddr := crypto.PubkeyToAddress(testKey.PublicKey) + var gas uint64 = 21000 + + sim := simTestBackend(testAddr) + defer sim.Close() + + var testCases = []struct { + name string + txType byte + }{ + { + name: "LegacyTx", + txType: types.LegacyTxType, + }, + { + name: "dynamicTx", + txType: types.DynamicFeeTxType, + }, + } + for i, test := range testCases { + t.Run(test.name, func(t *testing.T) { + bgCtx := context.Background() + // Create tx and send + head, _ := sim.HeaderByNumber(context.Background(), nil) + tip, err := sim.SuggestGasTipCap(bgCtx) + require.NoError(t, err) + gasPrice := new(big.Int).Add(head.BaseFee, tip) + sim.SetMarketGasPrice(int64(gasPrice.Uint64())) + + lowFeeGas := new(big.Int).Sub(gasPrice, tip) + txValue := big.NewInt(1) + + var lowGasFeeTx *types.Transaction + + if test.txType == types.LegacyTxType { + lowGasFeeTx = types.NewTx(&types.LegacyTx{ + Nonce: uint64(i), + To: &testAddr, + Value: txValue, + GasPrice: lowFeeGas, + Gas: gas, + }) + } else if test.txType == types.DynamicFeeTxType { + lowGasFeeTx = types.NewTx(&types.DynamicFeeTx{ + Nonce: uint64(i), + Gas: gas, + To: &testAddr, + Value: txValue, + GasTipCap: lowFeeGas, + GasFeeCap: lowFeeGas, + }) + } else { + t.Fatal("unexpected txType received") + } + + signedTx, err := types.SignTx(lowGasFeeTx, types.LatestSigner(sim.config), testKey) + require.NoError(t, err) + + // send tx to simulated backend + err = sim.SendTransaction(bgCtx, signedTx) + require.NoError(t, err) + sim.Commit() + + // expect nonce be the same because low gas fee tx will not be mined + nonce, err := sim.PendingNonceAt(bgCtx, testAddr) + require.NoError(t, err) + assert.Equal(t, uint64(i), nonce) + + // send tx with higher gas fee + sufficientGasFeeTx := types.NewTx(&types.LegacyTx{ + Nonce: uint64(i), + To: &testAddr, + Value: txValue, + GasPrice: gasPrice, + Gas: gas, + }) + + signedSufficientTx, err := types.SignTx(sufficientGasFeeTx, types.HomesteadSigner{}, testKey) + require.NoError(t, err) + + err = sim.SendTransaction(bgCtx, signedSufficientTx) + require.NoError(t, err) + sim.Commit() + + // the higher gas transaction should have been mined + _, isPending, err := sim.TransactionByHash(bgCtx, signedSufficientTx.Hash()) + require.NoError(t, err) + assert.False(t, isPending) + + // expect nonce has increased + nonce, err = sim.PendingNonceAt(bgCtx, testAddr) + require.NoError(t, err) + assert.Equal(t, uint64(i) + 1, nonce) + }) + } +} + +// TestLowGasTxGetMinedOnceGasFeeDropped checks that lower gas fee txes still stay in mem pool +// and get mined once gas fee requirement has been met +// Steps: +// 1. Send a tx lower than the set market gas price +// 2. Commit to chain, tx does not get mined +// 3. Gas fee drops, commit again +// 4. Check tx get mined +func TestLowGasTxGetMinedOnceGasFeeDropped(t *testing.T) { + var testCases = []struct { + name string + txType byte + }{ + { + name: "LegacyTx", + txType: types.LegacyTxType, + }, + { + name: "dynamicTx", + txType: types.DynamicFeeTxType, + }, + } + testAddr := crypto.PubkeyToAddress(testKey.PublicKey) + var gas uint64 = 21000 + + sim := simTestBackend(testAddr) + defer sim.Close() + + for i, test := range testCases { + t.Run(test.name, func(t *testing.T) { + bgCtx := context.Background() + // Create tx and send + head, _ := sim.HeaderByNumber(context.Background(), nil) + tip, err := sim.SuggestGasTipCap(bgCtx) + require.NoError(t, err) + normalGasPrice := new(big.Int).Add(head.BaseFee, tip) + highGasPrice := new(big.Int).Add(head.BaseFee, big.NewInt(5)) + + sim.SetMarketGasPrice(int64(highGasPrice.Uint64())) + + var normalGasFeeTx *types.Transaction + txValue := big.NewInt(1) + + if test.txType == types.LegacyTxType { + normalGasFeeTx = types.NewTx(&types.LegacyTx{ + Nonce: uint64(i), + To: &testAddr, + Value: txValue, + GasPrice: normalGasPrice, + Gas: gas, + }) + } else if test.txType == types.DynamicFeeTxType { + normalGasFeeTx = types.NewTx(&types.DynamicFeeTx{ + Nonce: uint64(i), + Gas: gas, + To: &testAddr, + Value: txValue, + GasTipCap: normalGasPrice, + GasFeeCap: normalGasPrice, + }) + } else { + t.Fatal("unexpected txType received") + } + + signedTx, err := types.SignTx(normalGasFeeTx, types.LatestSigner(sim.config), testKey) + require.NoError(t, err) + + // send tx to simulated backend + err = sim.SendTransaction(bgCtx, signedTx) + require.NoError(t, err) + sim.Commit() + + // check that the nonce stays the same because nothing has been mined + pendingNonce, err := sim.PendingNonceAt(bgCtx, testAddr) + require.NoError(t, err) + assert.Equal(t, uint64(i), pendingNonce) + + // gas fee has dropped + sim.SetMarketGasPrice(int64(normalGasPrice.Uint64())) + sim.Commit() + + // nonce has increased + pendingNonce, err = sim.PendingNonceAt(bgCtx, testAddr) + require.NoError(t, err) + assert.Equal(t, uint64(i) + 1, pendingNonce) + + // the transaction should have been mined + _, isPending, err := sim.TransactionByHash(bgCtx, signedTx.Hash()) + require.NoError(t, err) + assert.False(t, isPending) + }) + } +}