Commit f4b5c98
committed
fix(signer): price L1 transactions with a floor and escalate stalled ones
A transaction sent while the network is cheap can be priced below a later base
fee and become permanently unmineable, and because nonces are consumed in order
it then blocks every subsequent transaction from the same account. Nothing in
the signer revisited such a transaction: on timeout it returned an error and
left it in the mempool, so the queue only grew.
We hit this on a production proposer: 20 checkpointBlockHash transactions
accumulated behind a head priced at maxFeePerGas 0.3228 gwei while base fee had
risen to 1.25 gwei. 17 of the 20 were priced perfectly adequately and were held
up purely by nonce ordering. Recovery required replacing the transactions by
hand.
Note this failed at LOW gas, which is the opposite of the usual intuition about
congestion: automatic estimation derives maxFeePerGas from the current base fee,
so the cheaper the network the smaller the buffer, and base fee needs only a
handful of 12.5% steps to overtake it.
Two changes, both inside send_transaction_request_with_timeout so all three
signer kinds and every call site benefit:
- maxFeePerGas floor, default 3 gwei (L1_GAS_FEE_FLOOR_GWEI)
- gas price buffer, default 4x (L1_GAS_BASE_FEE_MULTIPLIER)
- on timeout, re-send at the SAME nonce with +25% on both fee fields,
up to 3 times (L1_TX_MAX_BUMPS)
The transactions this signer sends are small -- a checkpoint is ~46k gas -- so
a 3 gwei floor costs on the order of 0.00014 ETH per transaction, which is
negligible against a stalled proposer. All three are env-overridable so a live
incident can be handled by configuration rather than a deploy.
Three details carry the correctness:
- The nonce is pinned before the first send, so an escalation replaces the
transaction instead of queueing behind it. SignerLock already serialises
sends, so nothing else claims the nonce meanwhile.
- An escalation can lose the race: the transaction it replaces may be mined
just as the replacement goes out, and the node then rejects the
replacement. That is success, not failure. Every broadcast hash is recorded
before waiting for confirmation, and every error return is preceded by a
receipt lookup over all of them. Reporting a landed transaction as failed
would cause the caller to send a duplicate.
- Callers that price their own transaction keep those fees; both fee fields
must be set for that to apply, so a half-specified request is not paired
with a computed value.
When the attempts are exhausted the error distinguishes "still pending" from
"the nonce was consumed by a transaction we cannot identify", decided from the
on-chain nonce rather than by matching client-specific error text, since the
operator's next step differs.
Tests cover the floor, the buffer (asserted to survive >=12 consecutive blocks
of maximum base-fee climb), the EIP-1559 +10% replacement threshold across
values including zero, monotonic escalation, caller-fee precedence, and the
worst-case timing helper. Verified end to end against anvil with block
production withheld: the first attempt times out and the signer re-sends at the
same nonce, maxFeePerGas 15 -> 18 gwei, which then confirms.
Adds a tracing dependency to the signer crate so the escalation is visible;
previously nothing in it logged.1 parent e0f4902 commit f4b5c98
3 files changed
Lines changed: 411 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| |||
0 commit comments