Skip to content

Commit

Permalink
eth: Update replacement tx logic for EIP-1559
Browse files Browse the repository at this point in the history
  • Loading branch information
yondonfu committed Sep 30, 2021
1 parent fb0c308 commit 05da3f2
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 54 deletions.
72 changes: 37 additions & 35 deletions eth/transactionManager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
97 changes: 78 additions & 19 deletions eth/transactionManager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand All @@ -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")
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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,
})
}

0 comments on commit 05da3f2

Please sign in to comment.