1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package drivers
import (
"context"
"crypto/ecdsa"
"errors"
"math/big"
"strings"
"github.com/ethereum-optimism/optimism/bss-core/txmgr"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
// ErrClearPendingRetry signals that a transaction from a previous running
// instance confirmed rather than our clearing transaction on startup. In this
// case the caller should retry.
var ErrClearPendingRetry = errors.New("retry clear pending txn")
// ClearPendingTx publishes a NOOP transaction at the wallet's next unused
// nonce. This is used on restarts in order to clear the mempool of any prior
// publications and ensure the batch submitter starts submitting from a clean
// slate.
func ClearPendingTx(
name string,
ctx context.Context,
txMgr txmgr.TxManager,
l1Client L1Client,
walletAddr common.Address,
privKey *ecdsa.PrivateKey,
chainID *big.Int,
) error {
// Query for the submitter's current nonce.
nonce, err := l1Client.NonceAt(ctx, walletAddr, nil)
if err != nil {
log.Error(name+" unable to get current nonce",
"err", err)
return err
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Construct the clearing transaction submission clousure that will attempt
// to send the a clearing transaction transaction at the given nonce and gas
// price.
updateGasPrice := func(
ctx context.Context,
) (*types.Transaction, error) {
log.Info(name+" clearing pending tx", "nonce", nonce)
signedTx, err := SignClearingTx(
name, ctx, walletAddr, nonce, l1Client, privKey, chainID,
)
if err != nil {
log.Error(name+" unable to sign clearing tx", "nonce", nonce,
"err", err)
return nil, err
}
return signedTx, nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
txHash := tx.Hash()
gasTipCap := tx.GasTipCap()
gasFeeCap := tx.GasFeeCap()
err := l1Client.SendTransaction(ctx, tx)
switch {
// Clearing transaction successfully confirmed.
case err == nil:
log.Info(name+" submitted clearing tx", "nonce", nonce,
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash)
return nil
// Getting a nonce too low error implies that a previous transaction in
// the mempool has confirmed and we should abort trying to publish at
// this nonce.
case strings.Contains(err.Error(), core.ErrNonceTooLow.Error()):
log.Info(name + " transaction from previous restart confirmed, " +
"aborting mempool clearing")
cancel()
return context.Canceled
// An unexpected error occurred. This also handles the case where the
// clearing transaction has not yet bested the gas price a prior
// transaction in the mempool at this nonce. In such a case we will
// continue until our ratchetting strategy overtakes the old
// transaction, or abort if the old one confirms.
default:
log.Error(name+" unable to submit clearing tx",
"nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash, "err", err)
return err
}
}
receipt, err := txMgr.Send(ctx, updateGasPrice, sendTx)
switch {
// If the current context is canceled, a prior transaction in the mempool
// confirmed. The caller should retry, which will use the next nonce, before
// proceeding.
case err == context.Canceled:
log.Info(name + " transaction from previous restart confirmed, " +
"proceeding to startup")
return ErrClearPendingRetry
// Otherwise we were unable to confirm our transaction, this method should
// be retried by the caller.
case err != nil:
log.Warn(name+" unable to send clearing tx", "nonce", nonce,
"err", err)
return err
// We succeeded in confirming a clearing transaction. Proceed to startup as
// normal.
default:
log.Info(name+" cleared pending tx", "nonce", nonce,
"txHash", receipt.TxHash)
return nil
}
}
// SignClearingTx creates a signed clearing tranaction which sends 0 ETH back to
// the sender's address. EstimateGas is used to set an appropriate gas limit.
func SignClearingTx(
name string,
ctx context.Context,
walletAddr common.Address,
nonce uint64,
l1Client L1Client,
privKey *ecdsa.PrivateKey,
chainID *big.Int,
) (*types.Transaction, error) {
gasTipCap, err := l1Client.SuggestGasTipCap(ctx)
if err != nil {
if !IsMaxPriorityFeePerGasNotFoundError(err) {
return nil, err
}
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
// Currently Alchemy is the only backend provider that exposes this
// method, so in the event their API is unreachable we can fallback to a
// degraded mode of operation. This also applies to our test
// environments, as hardhat doesn't support the query either.
log.Warn(name + " eth_maxPriorityFeePerGas is unsupported " +
"by current backend, using fallback gasTipCap")
gasTipCap = FallbackGasTipCap
}
head, err := l1Client.HeaderByNumber(ctx, nil)
if err != nil {
return nil, err
}
gasFeeCap := txmgr.CalcGasFeeCap(head.BaseFee, gasTipCap)
gasLimit, err := l1Client.EstimateGas(ctx, ethereum.CallMsg{
From: walletAddr,
To: &walletAddr,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Value: nil,
Data: nil,
})
if err != nil {
return nil, err
}
tx := CraftClearingTx(walletAddr, nonce, gasFeeCap, gasTipCap, gasLimit)
return types.SignTx(
tx, types.LatestSignerForChainID(chainID), privKey,
)
}
// CraftClearingTx creates an unsigned clearing transaction which sends 0 ETH
// back to the sender's address.
func CraftClearingTx(
walletAddr common.Address,
nonce uint64,
gasFeeCap *big.Int,
gasTipCap *big.Int,
gasLimit uint64,
) *types.Transaction {
return types.NewTx(&types.DynamicFeeTx{
To: &walletAddr,
Nonce: nonce,
Gas: gasLimit,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Value: nil,
Data: nil,
})
}