Commit 09a63f3d authored by Joshua Gutow's avatar Joshua Gutow Committed by GitHub

Merge pull request #6031 from roberto-bayardo/fix-tx-replacement

require fee increase & gas re-estimation for retried transactions
parents cbae9e68 e1810d83
...@@ -30,51 +30,52 @@ func (tc *priceBumpTest) run(t *testing.T) { ...@@ -30,51 +30,52 @@ func (tc *priceBumpTest) run(t *testing.T) {
} }
func TestUpdateFees(t *testing.T) { func TestUpdateFees(t *testing.T) {
require.Equal(t, int64(10), priceBump, "test must be updated if priceBump is adjusted")
tests := []priceBumpTest{ tests := []priceBumpTest{
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
newGasTip: 90, newBasefee: 900, newGasTip: 90, newBasefee: 900,
expectedTip: 100, expectedFC: 2100, expectedTip: 110, expectedFC: 2310,
}, },
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 1000, newGasTip: 101, newBasefee: 1000,
expectedTip: 115, expectedFC: 2415, expectedTip: 110, expectedFC: 2310,
}, },
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
newGasTip: 100, newBasefee: 1001, newGasTip: 100, newBasefee: 1001,
expectedTip: 115, expectedFC: 2415, expectedTip: 110, expectedFC: 2310,
}, },
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 900, newGasTip: 101, newBasefee: 900,
expectedTip: 115, expectedFC: 2415, expectedTip: 110, expectedFC: 2310,
}, },
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
newGasTip: 90, newBasefee: 1010, newGasTip: 90, newBasefee: 1010,
expectedTip: 115, expectedFC: 2415, expectedTip: 110, expectedFC: 2310,
}, },
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 2000, newGasTip: 101, newBasefee: 2000,
expectedTip: 115, expectedFC: 4115, expectedTip: 110, expectedFC: 4110,
}, },
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 900, newGasTip: 120, newBasefee: 900,
expectedTip: 120, expectedFC: 2415, expectedTip: 120, expectedFC: 2310,
}, },
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 1100, newGasTip: 120, newBasefee: 1100,
expectedTip: 120, expectedFC: 2415, expectedTip: 120, expectedFC: 2320,
}, },
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 1140, newGasTip: 120, newBasefee: 1140,
expectedTip: 120, expectedFC: 2415, expectedTip: 120, expectedFC: 2400,
}, },
{ {
prevGasTip: 100, prevBasefee: 1000, prevGasTip: 100, prevBasefee: 1000,
......
...@@ -20,9 +20,13 @@ import ( ...@@ -20,9 +20,13 @@ import (
"github.com/ethereum-optimism/optimism/op-service/txmgr/metrics" "github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
) )
// Geth defaults the priceBump to 10 const (
// Set it to 15% to be more aggressive about including transactions // Geth requires a minimum fee bump of 10% for tx resubmission
const priceBump int64 = 15 priceBump int64 = 10
// The multiplier applied to fee suggestions to put a hard limit on fee increases
feeLimitMultiplier = 5
)
// new = old * (100 + priceBump) / 100 // new = old * (100 + priceBump) / 100
var priceBumpPercent = big.NewInt(100 + priceBump) var priceBumpPercent = big.NewInt(100 + priceBump)
...@@ -280,7 +284,15 @@ func (m *SimpleTxManager) sendTx(ctx context.Context, tx *types.Transaction) (*t ...@@ -280,7 +284,15 @@ func (m *SimpleTxManager) sendTx(ctx context.Context, tx *types.Transaction) (*t
return nil, errors.New("aborted transaction sending") return nil, errors.New("aborted transaction sending")
} }
// Increase the gas price & submit the new transaction // Increase the gas price & submit the new transaction
tx = m.increaseGasPrice(ctx, tx) newTx, err := m.increaseGasPrice(ctx, tx)
if err != nil || sendState.IsWaitingForConfirmation() {
// there is a chance the previous tx goes into "waiting for confirmation" state
// during the increaseGasPrice call. In some (but not all) cases increaseGasPrice
// will error out during gas estimation. In either case we should continue waiting
// rather than resubmit the tx.
continue
}
tx = newTx
wg.Add(1) wg.Add(1)
bumpCounter += 1 bumpCounter += 1
go sendTxAsync(tx) go sendTxAsync(tx)
...@@ -418,46 +430,67 @@ func (m *SimpleTxManager) queryReceipt(ctx context.Context, txHash common.Hash, ...@@ -418,46 +430,67 @@ func (m *SimpleTxManager) queryReceipt(ctx context.Context, txHash common.Hash,
return nil return nil
} }
// increaseGasPrice takes the previous transaction & potentially clones then signs it with a higher tip. // increaseGasPrice takes the previous transaction, clones it, and returns it with fee values that
// If the tip + basefee suggested by the network are not greater than the previous values, the same transaction // are at least `priceBump` percent higher than the previous ones to satisfy Geth's replacement
// will be returned. If they are greater, this function will ensure that they are at least greater by 15% than // rules, and no lower than the values returned by the fee suggestion algorithm to ensure it
// the previous transaction's value to ensure that the price bump is large enough. // doesn't linger in the mempool. Finally to avoid runaway price increases, fees are capped at a
// // `feeLimitMultiplier` multiple of the suggested values.
// We do not re-estimate the amount of gas used because for some stateful transactions (like output proposals) the func (m *SimpleTxManager) increaseGasPrice(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
// act of including the transaction renders the repeat of the transaction invalid. m.l.Info("bumping gas price for tx", "hash", tx.Hash(), "tip", tx.GasTipCap(), "fee", tx.GasFeeCap(), "gaslimit", tx.Gas())
//
// If it encounters an error with creating the new transaction, it will return the old transaction.
func (m *SimpleTxManager) increaseGasPrice(ctx context.Context, tx *types.Transaction) *types.Transaction {
tip, basefee, err := m.suggestGasPriceCaps(ctx) tip, basefee, err := m.suggestGasPriceCaps(ctx)
if err != nil { if err != nil {
m.l.Warn("failed to get suggested gas tip and basefee", "err", err) m.l.Warn("failed to get suggested gas tip and basefee", "err", err)
return tx return nil, err
} }
gasTipCap, gasFeeCap := updateFees(tx.GasTipCap(), tx.GasFeeCap(), tip, basefee, m.l) bumpedTip, bumpedFee := updateFees(tx.GasTipCap(), tx.GasFeeCap(), tip, basefee, m.l)
if tx.GasTipCapIntCmp(gasTipCap) == 0 && tx.GasFeeCapIntCmp(gasFeeCap) == 0 { // Make sure increase is at most 5x the suggested values
return tx maxTip := new(big.Int).Mul(tip, big.NewInt(feeLimitMultiplier))
if bumpedTip.Cmp(maxTip) > 0 {
m.l.Warn(fmt.Sprintf("bumped tip getting capped at %dx multiple of the suggested value", feeLimitMultiplier), "bumped", bumpedTip, "suggestion", tip)
bumpedTip.Set(maxTip)
}
maxFee := calcGasFeeCap(new(big.Int).Mul(basefee, big.NewInt(feeLimitMultiplier)), maxTip)
if bumpedFee.Cmp(maxFee) > 0 {
m.l.Warn("bumped fee getting capped at multiple of the implied suggested value", "bumped", bumpedFee, "suggestion", maxFee)
bumpedFee.Set(maxFee)
} }
rawTx := &types.DynamicFeeTx{ rawTx := &types.DynamicFeeTx{
ChainID: tx.ChainId(), ChainID: tx.ChainId(),
Nonce: tx.Nonce(), Nonce: tx.Nonce(),
GasTipCap: gasTipCap, GasTipCap: bumpedTip,
GasFeeCap: gasFeeCap, GasFeeCap: bumpedFee,
Gas: tx.Gas(),
To: tx.To(), To: tx.To(),
Value: tx.Value(), Value: tx.Value(),
Data: tx.Data(), Data: tx.Data(),
AccessList: tx.AccessList(), AccessList: tx.AccessList(),
} }
// Re-estimate gaslimit in case things have changed or a previous gaslimit estimate was wrong
gas, err := m.backend.EstimateGas(ctx, ethereum.CallMsg{
From: m.cfg.From,
To: rawTx.To,
GasFeeCap: bumpedTip,
GasTipCap: bumpedFee,
Data: rawTx.Data,
})
if err != nil {
m.l.Warn("failed to re-estimate gas", "err", err, "gaslimit", tx.Gas())
return nil, err
}
if tx.Gas() != gas {
m.l.Info("re-estimated gas differs", "oldgas", tx.Gas(), "newgas", gas)
}
rawTx.Gas = gas
ctx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout) ctx, cancel := context.WithTimeout(ctx, m.cfg.NetworkTimeout)
defer cancel() defer cancel()
newTx, err := m.cfg.Signer(ctx, m.cfg.From, types.NewTx(rawTx)) newTx, err := m.cfg.Signer(ctx, m.cfg.From, types.NewTx(rawTx))
if err != nil { if err != nil {
m.l.Warn("failed to sign new transaction", "err", err) m.l.Warn("failed to sign new transaction", "err", err)
return tx return tx, nil
} }
return newTx return newTx, nil
} }
// suggestGasPriceCaps suggests what the new tip & new basefee should be based on the current L1 conditions // suggestGasPriceCaps suggests what the new tip & new basefee should be based on the current L1 conditions
...@@ -490,18 +523,15 @@ func calcThresholdValue(x *big.Int) *big.Int { ...@@ -490,18 +523,15 @@ func calcThresholdValue(x *big.Int) *big.Int {
return threshold return threshold
} }
// updateFees takes the old tip/basefee & the new tip/basefee and then suggests // updateFees takes an old transaction's tip & fee cap plus a new tip & basefee, and returns
// a gasTipCap and gasFeeCap that satisfies geth's required fee bumps // a suggested tip and fee cap such that:
// Geth: FC and Tip must be bumped if any increase //
// (a) each satisfies geth's required tx-replacement fee bumps (we use a 10% increase), and
// (b) gasTipCap is no less than new tip, and
// (c) gasFeeCap is no less than calcGasFee(newBaseFee, newTip)
func updateFees(oldTip, oldFeeCap, newTip, newBaseFee *big.Int, lgr log.Logger) (*big.Int, *big.Int) { func updateFees(oldTip, oldFeeCap, newTip, newBaseFee *big.Int, lgr log.Logger) (*big.Int, *big.Int) {
newFeeCap := calcGasFeeCap(newBaseFee, newTip) newFeeCap := calcGasFeeCap(newBaseFee, newTip)
lgr = lgr.New("old_tip", oldTip, "old_feecap", oldFeeCap, "new_tip", newTip, "new_feecap", newFeeCap) lgr = lgr.New("old_tip", oldTip, "old_feecap", oldFeeCap, "new_tip", newTip, "new_feecap", newFeeCap)
// If the new prices are less than the old price, reuse the old prices
if oldTip.Cmp(newTip) >= 0 && oldFeeCap.Cmp(newFeeCap) >= 0 {
lgr.Debug("Reusing old tip and feecap")
return oldTip, oldFeeCap
}
// Determine if we need to increase the suggested values
thresholdTip := calcThresholdValue(oldTip) thresholdTip := calcThresholdValue(oldTip)
thresholdFeeCap := calcThresholdValue(oldFeeCap) thresholdFeeCap := calcThresholdValue(oldFeeCap)
if newTip.Cmp(thresholdTip) >= 0 && newFeeCap.Cmp(thresholdFeeCap) >= 0 { if newTip.Cmp(thresholdTip) >= 0 && newFeeCap.Cmp(thresholdFeeCap) >= 0 {
......
...@@ -734,12 +734,14 @@ func doGasPriceIncrease(t *testing.T, txTipCap, txFeeCap, newTip, newBaseFee int ...@@ -734,12 +734,14 @@ func doGasPriceIncrease(t *testing.T, txTipCap, txFeeCap, newTip, newBaseFee int
GasTipCap: big.NewInt(txTipCap), GasTipCap: big.NewInt(txTipCap),
GasFeeCap: big.NewInt(txFeeCap), GasFeeCap: big.NewInt(txFeeCap),
}) })
newTx := mgr.increaseGasPrice(context.Background(), tx) newTx, err := mgr.increaseGasPrice(context.Background(), tx)
require.NoError(t, err)
return tx, newTx return tx, newTx
} }
func TestIncreaseGasPrice(t *testing.T) { func TestIncreaseGasPrice(t *testing.T) {
// t.Parallel() // t.Parallel()
require.Equal(t, int64(10), priceBump, "test must be updated if priceBump is adjusted")
tests := []struct { tests := []struct {
name string name string
run func(t *testing.T) run func(t *testing.T)
...@@ -781,22 +783,16 @@ func TestIncreaseGasPrice(t *testing.T) { ...@@ -781,22 +783,16 @@ func TestIncreaseGasPrice(t *testing.T) {
run: func(t *testing.T) { run: func(t *testing.T) {
_, newTx := doGasPriceIncrease(t, 100, 2200, 120, 1050) _, newTx := doGasPriceIncrease(t, 100, 2200, 120, 1050)
require.True(t, newTx.GasTipCap().Cmp(big.NewInt(120)) == 0, "new tx tip must be equal L1") require.True(t, newTx.GasTipCap().Cmp(big.NewInt(120)) == 0, "new tx tip must be equal L1")
require.True(t, newTx.GasFeeCap().Cmp(big.NewInt(2530)) == 0, "new tx fee cap must be equal to the threshold value") require.True(t, newTx.GasFeeCap().Cmp(big.NewInt(2420)) == 0, "new tx fee cap must be equal to the threshold value")
}, },
}, },
{ {
name: "uses L1 FC when larger and threshold tip", name: "uses L1 FC when larger and threshold tip",
run: func(t *testing.T) { run: func(t *testing.T) {
_, newTx := doGasPriceIncrease(t, 100, 2200, 100, 2000) _, newTx := doGasPriceIncrease(t, 100, 2200, 100, 2000)
require.True(t, newTx.GasTipCap().Cmp(big.NewInt(115)) == 0, "new tx tip must be equal the threshold value") require.True(t, newTx.GasTipCap().Cmp(big.NewInt(110)) == 0, "new tx tip must be equal the threshold value")
require.True(t, newTx.GasFeeCap().Cmp(big.NewInt(4115)) == 0, "new tx fee cap must be equal L1") t.Log("Vals:", newTx.GasFeeCap())
}, require.True(t, newTx.GasFeeCap().Cmp(big.NewInt(4110)) == 0, "new tx fee cap must be equal L1")
},
{
name: "reuses tx when no bump",
run: func(t *testing.T) {
tx, newTx := doGasPriceIncrease(t, 10, 100, 10, 45)
require.Equal(t, tx.Hash(), newTx.Hash(), "tx hash must be the same")
}, },
}, },
} }
...@@ -811,11 +807,12 @@ func TestIncreaseGasPrice(t *testing.T) { ...@@ -811,11 +807,12 @@ func TestIncreaseGasPrice(t *testing.T) {
func TestIncreaseGasPriceNotExponential(t *testing.T) { func TestIncreaseGasPriceNotExponential(t *testing.T) {
t.Parallel() t.Parallel()
borkedTip := int64(10)
borkedFee := int64(45)
borkedBackend := failingBackend{ borkedBackend := failingBackend{
gasTip: big.NewInt(10), gasTip: big.NewInt(borkedTip),
baseFee: big.NewInt(45), baseFee: big.NewInt(borkedFee),
} }
feeCap := calcGasFeeCap(borkedBackend.baseFee, borkedBackend.gasTip)
mgr := &SimpleTxManager{ mgr := &SimpleTxManager{
cfg: Config{ cfg: Config{
...@@ -839,14 +836,23 @@ func TestIncreaseGasPriceNotExponential(t *testing.T) { ...@@ -839,14 +836,23 @@ func TestIncreaseGasPriceNotExponential(t *testing.T) {
}) })
// Run IncreaseGasPrice a bunch of times in a row to simulate a very fast resubmit loop. // Run IncreaseGasPrice a bunch of times in a row to simulate a very fast resubmit loop.
for i := 0; i < 20; i++ { var err error
for i := 0; i < 30; i++ {
ctx := context.Background() ctx := context.Background()
newTx := mgr.increaseGasPrice(ctx, tx) tx, err = mgr.increaseGasPrice(ctx, tx)
require.True(t, newTx.GasFeeCap().Cmp(feeCap) == 0, "new tx fee cap must be equal L1") require.NoError(t, err)
require.True(t, newTx.GasTipCap().Cmp(borkedBackend.gasTip) == 0, "new tx tip must be equal L1") }
tx = newTx lastTip, lastFee := tx.GasTipCap(), tx.GasFeeCap()
require.Equal(t, lastTip.Int64(), feeLimitMultiplier*borkedTip)
require.Equal(t, lastFee.Int64(), feeLimitMultiplier*(borkedTip+2*borkedFee))
// Confirm that fees stop rising
for i := 0; i < 5; i++ {
ctx := context.Background()
tx, err := mgr.increaseGasPrice(ctx, tx)
require.NoError(t, err)
require.True(t, tx.GasTipCap().Cmp(lastTip) == 0, "suggested tx tip must stop increasing")
require.True(t, tx.GasFeeCap().Cmp(lastFee) == 0, "suggested tx fee must stop increasing")
} }
} }
func TestErrStringMatch(t *testing.T) { func TestErrStringMatch(t *testing.T) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment