Skip to content

Commit

Permalink
add market gas price to simulate duplicate nonce behavior (paxosglobal#7
Browse files Browse the repository at this point in the history
)
  • Loading branch information
susannapaxos authored and blakehhuynh committed Oct 3, 2022
1 parent ea26fc8 commit 7018761
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 11 deletions.
60 changes: 49 additions & 11 deletions accounts/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,18 @@ 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
pendingReceipts types.Receipts // Currently receipts for the pending block
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
pendingReceipts types.Receipts // Currently receipts for the pending block

events *filters.EventSystem // for filtering log events live
filterSystem *filters.FilterSystem // for filtering database logs

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 @@ -118,15 +121,30 @@ func (b *SimulatedBackend) Commit() common.Hash {
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
}
blockHash := b.pendingBlock.Hash()

// 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])
return blockHash
}

Expand All @@ -140,7 +158,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)
}
Expand Down Expand Up @@ -223,6 +240,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()
Expand Down Expand Up @@ -674,18 +700,30 @@ func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transa
if tx.Nonce() != nonce {
return fmt.Errorf("invalid transaction nonce: got %d, want %d", tx.Nonce(), nonce)
}
var remainingTxes types.Transactions
// Include tx in chain
blocks, receipts := core.GenerateChain(b.config, block, ethash.NewFaker(), b.database, 1, func(number int, block *core.BlockGen) {
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.pendingReceipts = receipts[0]
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 @@ -1397,3 +1400,201 @@ func TestAdjustTimeAfterFork(t *testing.T) {
t.Errorf("failed to build block on fork")
}
}

// 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)
})
}
}

0 comments on commit 7018761

Please sign in to comment.