Skip to content

Commit 81ef8e6

Browse files
committed
OEV-668 Add gas bumping
1 parent 91c5a2d commit 81ef8e6

File tree

5 files changed

+194
-87
lines changed

5 files changed

+194
-87
lines changed

pkg/txm/attempt_builder.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import (
1515
"github.com/smartcontractkit/chainlink-evm/pkg/txm/types"
1616
)
1717

18+
// maxBumpThreshold controls the maximum number of bumps for an attempt.
19+
const maxBumpThreshold = 5
20+
1821
type attemptBuilder struct {
1922
gas.EvmFeeEstimator
2023
priceMaxKey func(common.Address) *assets.Wei
@@ -59,6 +62,23 @@ func (a *attemptBuilder) NewBumpAttempt(ctx context.Context, lggr logger.Logger,
5962
return a.newCustomAttempt(ctx, tx, bumpedFee, bumpedFeeLimit, previousAttempt.Type, lggr)
6063
}
6164

65+
func (a *attemptBuilder) NewAgnosticBumpAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (*types.Attempt, error) {
66+
attempt, err := a.NewAttempt(ctx, lggr, tx, dynamic)
67+
if err != nil {
68+
return nil, err
69+
}
70+
bumps := min(maxBumpThreshold, tx.AttemptCount)
71+
for range bumps {
72+
bumpedAttempt, err := a.NewBumpAttempt(ctx, lggr, tx, *attempt)
73+
if err != nil {
74+
lggr.Errorf("error bumping attempt: %v for txID: %v", err, tx.ID)
75+
return attempt, nil
76+
}
77+
attempt = bumpedAttempt
78+
}
79+
return attempt, nil
80+
}
81+
6282
func (a *attemptBuilder) newCustomAttempt(
6383
ctx context.Context,
6484
tx *types.Transaction,

pkg/txm/attempt_builder_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package txm
22

33
import (
4+
"errors"
45
"testing"
56

7+
"github.com/ethereum/go-ethereum/common"
68
evmtypes "github.com/ethereum/go-ethereum/core/types"
9+
710
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/mock"
812
"github.com/stretchr/testify/require"
913

1014
"github.com/smartcontractkit/chainlink-common/pkg/logger"
1115
"github.com/smartcontractkit/chainlink-evm/pkg/assets"
1216
"github.com/smartcontractkit/chainlink-evm/pkg/gas"
17+
"github.com/smartcontractkit/chainlink-evm/pkg/gas/mocks"
1318
"github.com/smartcontractkit/chainlink-evm/pkg/keys/keystest"
1419
"github.com/smartcontractkit/chainlink-evm/pkg/testutils"
1520
"github.com/smartcontractkit/chainlink-evm/pkg/txm/types"
@@ -85,3 +90,147 @@ func TestAttemptBuilder_newDynamicFeeAttempt(t *testing.T) {
8590
assert.Equal(t, gasLimit, a.GasLimit)
8691
})
8792
}
93+
94+
func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) {
95+
address := testutils.NewAddress()
96+
lggr := logger.Test(t)
97+
var nonce uint64 = 77
98+
priceMaxKey := func(addr common.Address) *assets.Wei {
99+
return assets.NewWeiI(1000)
100+
}
101+
102+
t.Run("returns original attempt when AttemptCount is 0", func(t *testing.T) {
103+
mockEstimator := mocks.NewEvmFeeEstimator(t)
104+
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
105+
106+
tx := &types.Transaction{
107+
ID: 10,
108+
FromAddress: address,
109+
Nonce: &nonce,
110+
AttemptCount: 0,
111+
}
112+
113+
gasPrice := assets.NewWeiI(100)
114+
initialFee := gas.EvmFee{GasPrice: gasPrice}
115+
mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
116+
Return(initialFee, uint64(21000), nil).Once()
117+
118+
attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false)
119+
require.NoError(t, err)
120+
assert.Equal(t, tx.ID, attempt.TxID)
121+
assert.Equal(t, gasPrice.String(), attempt.Fee.GasPrice.String())
122+
assert.Equal(t, evmtypes.LegacyTxType, int(attempt.Type))
123+
mockEstimator.AssertExpectations(t)
124+
})
125+
126+
t.Run("bumps once when AttemptCount is 1", func(t *testing.T) {
127+
mockEstimator := mocks.NewEvmFeeEstimator(t)
128+
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
129+
130+
tx := &types.Transaction{
131+
ID: 10,
132+
FromAddress: address,
133+
Nonce: &nonce,
134+
AttemptCount: 1,
135+
}
136+
137+
gasPrice := assets.NewWeiI(100)
138+
initialFee := gas.EvmFee{GasPrice: gasPrice}
139+
bumpedFee := gas.EvmFee{GasPrice: gasPrice.Add(assets.NewWeiI(20))}
140+
mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
141+
Return(initialFee, uint64(21000), nil).Once()
142+
mockEstimator.On("BumpFee", mock.Anything, initialFee, mock.Anything, mock.Anything, mock.Anything).
143+
Return(bumpedFee, uint64(21000), nil).Once()
144+
145+
attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false)
146+
require.NoError(t, err)
147+
assert.Equal(t, tx.ID, attempt.TxID)
148+
assert.Equal(t, bumpedFee.GasPrice.String(), attempt.Fee.GasPrice.String())
149+
mockEstimator.AssertExpectations(t)
150+
})
151+
152+
t.Run("bumps N times when AttemptCount is N", func(t *testing.T) {
153+
mockEstimator := mocks.NewEvmFeeEstimator(t)
154+
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
155+
156+
tx := &types.Transaction{
157+
ID: 10,
158+
FromAddress: address,
159+
Nonce: &nonce,
160+
AttemptCount: 3,
161+
}
162+
163+
initialFee := gas.EvmFee{GasPrice: assets.NewWeiI(100)}
164+
firstBump := gas.EvmFee{GasPrice: assets.NewWeiI(110)}
165+
secondBump := gas.EvmFee{GasPrice: assets.NewWeiI(121)}
166+
thirdBump := gas.EvmFee{GasPrice: assets.NewWeiI(133)}
167+
mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
168+
Return(initialFee, uint64(21000), nil).Once()
169+
mockEstimator.On("BumpFee", mock.Anything, initialFee, mock.Anything, mock.Anything, mock.Anything).
170+
Return(firstBump, uint64(21000), nil).Once()
171+
mockEstimator.On("BumpFee", mock.Anything, firstBump, mock.Anything, mock.Anything, mock.Anything).
172+
Return(secondBump, uint64(21000), nil).Once()
173+
mockEstimator.On("BumpFee", mock.Anything, secondBump, mock.Anything, mock.Anything, mock.Anything).
174+
Return(thirdBump, uint64(21000), nil).Once()
175+
176+
attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false)
177+
require.NoError(t, err)
178+
assert.Equal(t, tx.ID, attempt.TxID)
179+
assert.Equal(t, thirdBump.GasPrice.String(), attempt.Fee.GasPrice.String())
180+
mockEstimator.AssertExpectations(t)
181+
})
182+
183+
t.Run("returns last valid attempt when BumpFee fails", func(t *testing.T) {
184+
mockEstimator := mocks.NewEvmFeeEstimator(t)
185+
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
186+
187+
tx := &types.Transaction{
188+
ID: 10,
189+
FromAddress: address,
190+
Nonce: &nonce,
191+
AttemptCount: 3,
192+
}
193+
194+
gasPrice := assets.NewWeiI(100)
195+
initialFee := gas.EvmFee{GasPrice: gasPrice}
196+
firstBump := gas.EvmFee{GasPrice: gasPrice.Add(assets.NewWeiI(20))}
197+
mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
198+
Return(initialFee, uint64(21000), nil).Once()
199+
mockEstimator.On("BumpFee", mock.Anything, initialFee, mock.Anything, mock.Anything, mock.Anything).
200+
Return(firstBump, uint64(21000), nil).Once()
201+
mockEstimator.On("BumpFee", mock.Anything, firstBump, mock.Anything, mock.Anything, mock.Anything).
202+
Return(gas.EvmFee{}, uint64(0), errors.New("transaction propagation issue: transactions are not being mined")).Once()
203+
204+
attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false)
205+
require.NoError(t, err)
206+
assert.Equal(t, tx.ID, attempt.TxID)
207+
// Should return the last valid bumped attempt
208+
assert.Equal(t, firstBump.GasPrice.String(), attempt.Fee.GasPrice.String())
209+
mockEstimator.AssertExpectations(t)
210+
})
211+
212+
t.Run("caps bumps at maxBumpThreshold", func(t *testing.T) {
213+
mockEstimator := mocks.NewEvmFeeEstimator(t)
214+
ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100)
215+
216+
tx := &types.Transaction{
217+
ID: 10,
218+
FromAddress: address,
219+
Nonce: &nonce,
220+
AttemptCount: 10, // More than maxBumpThreshold (5)
221+
}
222+
223+
initialFee := gas.EvmFee{GasPrice: assets.NewWeiI(100)}
224+
bumpedFee := gas.EvmFee{GasPrice: initialFee.GasPrice.Add(assets.NewWeiI(20))}
225+
mockEstimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
226+
Return(initialFee, uint64(21000), nil).Once()
227+
// Should only bump 5 times (maxBumpThreshold)
228+
mockEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
229+
Return(bumpedFee, uint64(21000), nil).Times(5)
230+
231+
attempt, err := ab.NewAgnosticBumpAttempt(t.Context(), lggr, tx, false)
232+
require.NoError(t, err)
233+
assert.Equal(t, tx.ID, attempt.TxID)
234+
mockEstimator.AssertExpectations(t)
235+
})
236+
}

pkg/txm/mock_attempt_builder_test.go

Lines changed: 19 additions & 80 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/txm/txm.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ type TxStore interface {
4949
}
5050

5151
type AttemptBuilder interface {
52-
NewAttempt(context.Context, logger.Logger, *types.Transaction, bool) (*types.Attempt, error)
53-
NewBumpAttempt(context.Context, logger.Logger, *types.Transaction, types.Attempt) (*types.Attempt, error)
52+
NewAgnosticBumpAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (*types.Attempt, error)
5453
}
5554

5655
type ErrorHandler interface {
@@ -305,7 +304,7 @@ func (t *Txm) broadcastTransaction(ctx context.Context, address common.Address)
305304
}
306305

307306
func (t *Txm) createAndSendAttempt(ctx context.Context, tx *types.Transaction, address common.Address) error {
308-
attempt, err := t.attemptBuilder.NewAttempt(ctx, t.lggr, tx, t.config.EIP1559)
307+
attempt, err := t.attemptBuilder.NewAgnosticBumpAttempt(ctx, t.lggr, tx, t.config.EIP1559)
309308
if err != nil {
310309
return err
311310
}

0 commit comments

Comments
 (0)