Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add market gas price to simulate duplicate nonce behavior #7

Merged
merged 4 commits into from
Sep 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions accounts/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link

@lsharir lsharir Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we instead just put them back on the pending block?
similarly to how SendTransaction recreates the pendingBlock

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cannot put them to the pending block, the txes in the pending block determine whats the next nonce available. if they are stuck, they should not be in pending block. because that means we cannot send the transaction with the same nonce. it will be invalid/nonce too low.

b.rollback(blocks[0])
}

// Rollback aborts all pending transactions, reverting to the last committed state.
Expand All @@ -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) {})
susannapaxos marked this conversation as resolved.
Show resolved Hide resolved

b.pendingBlock = blocks[0]
b.pendingState, _ = state.New(b.pendingBlock.Root(), b.blockchain.StateCache(), nil)
}
Expand Down Expand Up @@ -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) {
gitteri marked this conversation as resolved.
Show resolved Hide resolved
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()
Expand Down Expand Up @@ -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
}

Expand Down
201 changes: 201 additions & 0 deletions accounts/abi/bind/backends/simulated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
})
}
}