diff --git a/eth/transactionManager.go b/eth/transactionManager.go index cd2161d9cc..c4d859b4ee 100644 --- a/eth/transactionManager.go +++ b/eth/transactionManager.go @@ -17,6 +17,11 @@ import ( "github.com/livepeer/go-livepeer/common" ) +// The default price bump required by geth is 10% +// We add a little extra in addition to the 10% price bump just to be safe +// priceBump is a % value from 0-100 +const priceBump uint64 = 11 + type transactionSenderReader interface { ethereum.TransactionSender ethereum.TransactionReader @@ -150,29 +155,15 @@ func (tm *TransactionManager) replace(tx *types.Transaction) (*types.Transaction return nil, ErrReplacingMinedTx } - gasPrice := calcReplacementGasPrice(tx) - - suggestedGasPrice := tm.gpm.GasPrice() - - // If the suggested gas price is higher than the bumped gas price, use the suggested gas price - // This is to account for any wild market gas price increases between the time of the original tx submission and time - // of replacement tx submission - // Note: If the suggested gas price is lower than the bumped gas price because market gas prices have dropped - // since the time of the original tx submission we cannot use the lower suggested gas price and we still need to use - // the bumped gas price in order to properly replace a still pending tx - if suggestedGasPrice.Cmp(gasPrice) == 1 { - gasPrice = suggestedGasPrice - } + newRawTx := newReplacementTx(tx) // Bump gas price exceeds max gas price, return early max := tm.gpm.MaxGasPrice() - if max != nil && gasPrice.Cmp(max) > 0 { - return nil, fmt.Errorf("replacement gas price exceeds max gas price suggested=%v max=%v", gasPrice, max) + newGasPrice := calcGasPrice(newRawTx) + if max != nil && newGasPrice.Cmp(max) > 0 { + return nil, fmt.Errorf("replacement gas price exceeds max gas price suggested=%v max=%v", newGasPrice, max) } - // Replacement raw tx uses same fields as old tx (reusing the same nonce is crucial) except the gas price is updated - newRawTx := types.NewTransaction(tx.Nonce(), *tx.To(), tx.Value(), tx.Gas(), gasPrice, tx.Data()) - newSignedTx, err := tm.sig.SignTx(newRawTx) if err != nil { return nil, err @@ -184,9 +175,9 @@ func (tm *TransactionManager) replace(tx *types.Transaction) (*types.Transaction txLog.method = "unknown" } if sendErr != nil { - glog.Infof("\n%vEth Transaction%v\n\nReplacement transaction: \"%v\". Gas Price: %v \nTransaction Failed: %v\n\n%v\n", strings.Repeat("*", 30), strings.Repeat("*", 30), txLog.method, newSignedTx.GasPrice().String(), sendErr, strings.Repeat("*", 75)) + glog.Infof("\n%vEth Transaction%v\n\nReplacement transaction: \"%v\". Priority Fee: %v Max Fee: %v \nTransaction Failed: %v\n\n%v\n", strings.Repeat("*", 30), strings.Repeat("*", 30), txLog.method, newSignedTx.GasTipCap().String(), newSignedTx.GasFeeCap().String(), sendErr, strings.Repeat("*", 75)) } else { - glog.Infof("\n%vEth Transaction%v\n\nReplacement transaction: \"%v\". Hash: \"%v\". Gas Price: %v \n\n%v\n", strings.Repeat("*", 30), strings.Repeat("*", 30), txLog.method, newSignedTx.Hash().String(), newSignedTx.GasPrice().String(), strings.Repeat("*", 75)) + glog.Infof("\n%vEth Transaction%v\n\nReplacement transaction: \"%v\". Hash: \"%v\". Priority Fee: %v Max Fee: %v \n\n%v\n", strings.Repeat("*", 30), strings.Repeat("*", 30), txLog.method, newSignedTx.Hash().String(), newSignedTx.GasTipCap().String(), newSignedTx.GasFeeCap().String(), strings.Repeat("*", 75)) } return newSignedTx, sendErr @@ -243,19 +234,30 @@ func (tm *TransactionManager) checkTxLoop() { } } -// Updated gas price must be at least 10% greater than the gas price used for the original transaction in order -// to submit a replacement transaction with the same nonce. 10% is not defined by the protocol, but is the default required price bump -// used by many clients: https://github.com/ethereum/go-ethereum/blob/01a7e267dc6d7bbef94882542bbd01bd712f5548/core/tx_pool.go#L148 -// We add a little extra in addition to the 10% price bump just to be sure -func calcReplacementGasPrice(tx *types.Transaction) *big.Int { - return new(big.Int).Add( - new(big.Int).Add( - tx.GasPrice(), - new(big.Int).Div( - tx.GasPrice(), - big.NewInt(10), - ), - ), - big.NewInt(10), - ) +func applyPriceBump(val *big.Int, priceBump uint64) *big.Int { + a := big.NewInt(100 + int64(priceBump)) + b := new(big.Int).Mul(a, val) + return b.Div(b, big.NewInt(100)) +} + +// Calculate the gas price as gas tip cap + base fee +func calcGasPrice(tx *types.Transaction) *big.Int { + // Assume that the gas fee cap is calculated as gas tip cap + (baseFee * 2) + baseFee := new(big.Int).Div(new(big.Int).Sub(tx.GasFeeCap(), tx.GasTipCap()), big.NewInt(2)) + return new(big.Int).Add(baseFee, tx.GasTipCap()) +} + +func newReplacementTx(tx *types.Transaction) *types.Transaction { + baseTx := &types.DynamicFeeTx{ + Nonce: tx.Nonce(), + // geth requires the price bump to be applied to both the gas tip cap and gas fee cap + GasFeeCap: applyPriceBump(tx.GasFeeCap(), priceBump), + GasTipCap: applyPriceBump(tx.GasTipCap(), priceBump), + Gas: tx.Gas(), + Value: tx.Value(), + Data: tx.Data(), + To: tx.To(), + } + + return types.NewTx(baseTx) } diff --git a/eth/transactionManager_test.go b/eth/transactionManager_test.go index bceefd5a61..924ce5d309 100644 --- a/eth/transactionManager_test.go +++ b/eth/transactionManager_test.go @@ -15,7 +15,6 @@ import ( "github.com/golang/glog" "github.com/livepeer/go-livepeer/pm" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) type stubTransactionSenderReader struct { @@ -150,13 +149,14 @@ func TestTransactionManager_Wait(t *testing.T) { func TestTransactionManager_Replace(t *testing.T) { assert := assert.New(t) - require := require.New(t) eth := &stubTransactionSenderReader{ err: make(map[string]error), } q := transactionQueue{} - gasPrice := big.NewInt(10) + baseFee := big.NewInt(9) + gasTipCap := big.NewInt(1) + gasFeeCap := new(big.Int).Add(gasTipCap, new(big.Int).Mul(baseFee, big.NewInt(2))) gpm := &GasPriceMonitor{ minGasPrice: big.NewInt(0), maxGasPrice: big.NewInt(0), @@ -170,7 +170,7 @@ func TestTransactionManager_Replace(t *testing.T) { gpm: gpm, } - stubTx := types.NewTransaction(1, pm.RandAddress(), big.NewInt(100), 100000, gasPrice, pm.RandBytes(68)) + stubTx := newStubDynamicFeeTx(gasFeeCap, gasTipCap) // Test eth.TransactionByHash error expErr := errors.New("TransactionByHash error") @@ -195,7 +195,7 @@ func TestTransactionManager_Replace(t *testing.T) { assert.Nil(tx) assert.EqualError( err, - fmt.Sprintf("replacement gas price exceeds max gas price suggested=%v max=%v", calcReplacementGasPrice(stubTx), gpm.maxGasPrice), + fmt.Sprintf("replacement gas price exceeds max gas price suggested=%v max=%v", applyPriceBump(calcGasPrice(stubTx), priceBump), gpm.maxGasPrice), ) eth.err["TransactionByHash"] = nil @@ -205,7 +205,7 @@ func TestTransactionManager_Replace(t *testing.T) { assert.Nil(tx) assert.EqualError( err, - fmt.Sprintf("replacement gas price exceeds max gas price suggested=%v max=%v", calcReplacementGasPrice(stubTx), gpm.maxGasPrice), + fmt.Sprintf("replacement gas price exceeds max gas price suggested=%v max=%v", applyPriceBump(calcGasPrice(stubTx), priceBump), gpm.maxGasPrice), ) // Error signing replacement tx @@ -242,19 +242,7 @@ func TestTransactionManager_Replace(t *testing.T) { tx, err = tm.replace(stubTx) logsAfter = glog.Stats.Info.Lines() assert.Nil(err) - expTx := types.NewTransaction(1, *stubTx.To(), stubTx.Value(), 100000, calcReplacementGasPrice(stubTx), stubTx.Data()) - assert.Equal(tx.Hash(), expTx.Hash()) - assert.Equal(logsAfter-logsBefore, int64(1)) - - // Replacement gas price lower than suggest gas price - // Use market gas price - gpm.gasPrice = big.NewInt(999) - require.True(gpm.GasPrice().Cmp(calcReplacementGasPrice(stubTx)) > 0) - logsBefore = glog.Stats.Info.Lines() - tx, err = tm.replace(stubTx) - logsAfter = glog.Stats.Info.Lines() - assert.Nil(err) - expTx = types.NewTransaction(1, *stubTx.To(), stubTx.Value(), 100000, gpm.gasPrice, stubTx.Data()) + expTx := newReplacementTx(stubTx) assert.Equal(tx.Hash(), expTx.Hash()) assert.Equal(logsAfter-logsBefore, int64(1)) } @@ -367,3 +355,74 @@ func TestTransactionManager_CheckTxLoop(t *testing.T) { assert.Equal(eth.callsToTxByHash, tm.maxReplacements) sub.Unsubscribe() } + +func TestApplyPriceBump(t *testing.T) { + assert := assert.New(t) + + // priceBump = 0 + // 500 * 1 = 500 + res := applyPriceBump(big.NewInt(500), 0) + assert.Equal(big.NewInt(500), res) + + // priceBump = 0.11 + // 500 * 1.11 = 555 + res = applyPriceBump(big.NewInt(500), 11) + assert.Equal(big.NewInt(555), res) + + // priceBump = 0.17 + // 500 * 1.17 = 585 + res = applyPriceBump(big.NewInt(500), 17) + assert.Equal(big.NewInt(585), res) + + // priceBump > 100 + // 500 * 2.01 = 1005 + res = applyPriceBump(big.NewInt(500), 101) + assert.Equal(big.NewInt(1005), res) + + // Test round down when result is not a whole number + // 50 * 1.11 = 55.5 -> 55 + res = applyPriceBump(big.NewInt(50), 11) + assert.Equal(big.NewInt(55), res) +} + +func TestCalcGasPrice(t *testing.T) { + assert := assert.New(t) + + baseFee := big.NewInt(1000) + gasTipCap := big.NewInt(100) + gasFeeCap := new(big.Int).Add(gasTipCap, new(big.Int).Mul(baseFee, big.NewInt(2))) + tx := newStubDynamicFeeTx(gasFeeCap, gasTipCap) + + gasPrice := calcGasPrice(tx) + assert.Equal(new(big.Int).Add(baseFee, gasTipCap), gasPrice) +} + +func TestNewReplacementTx(t *testing.T) { + assert := assert.New(t) + + gasTipCap := big.NewInt(100) + gasFeeCap := big.NewInt(1000) + + tx1 := newStubDynamicFeeTx(gasFeeCap, gasTipCap) + tx2 := newReplacementTx(tx1) + assert.NotEqual(tx1.Hash(), tx2.Hash()) + assert.Equal(applyPriceBump(tx1.GasTipCap(), priceBump), tx2.GasTipCap()) + assert.Equal(applyPriceBump(tx1.GasFeeCap(), priceBump), tx2.GasFeeCap()) + assert.Equal(tx1.Nonce(), tx2.Nonce()) + assert.Equal(tx1.Gas(), tx2.Gas()) + assert.Equal(tx1.Value(), tx1.Value()) + assert.Equal(tx1.To(), tx2.To()) +} + +func newStubDynamicFeeTx(gasFeeCap, gasTipCap *big.Int) *types.Transaction { + addr := pm.RandAddress() + return types.NewTx(&types.DynamicFeeTx{ + Nonce: 1, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Gas: 1000000, + Value: big.NewInt(100), + Data: pm.RandBytes(68), + To: &addr, + }) +}