You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stellar sequence numbers are handled by the client and surface as a tx_bad_seq error the user
retries — see explain_code in submit.rs. EVM nonces
cannot be treated that way for server-originated transactions (gas funding and sweeps, #224), because
the server owns those accounts and must sequence them itself.
EVM adds three problems Stellar simply does not have:
Nonce gaps block the account. Nonces must be strictly sequential. Transaction n+1 cannot
mine until n does. One stuck transaction halts every subsequent one on that account.
Transactions get stuck. An underpriced transaction can sit in the mempool indefinitely. The
only fix is replacement — resubmitting the same nonce with ≥ 10% higher gas.
Transactions can be dropped. A mempool eviction means a transaction you believe is pending
simply does not exist any more, with no notification.
Build nonce management and a transaction lifecycle tracker for server-originated transactions.
Requirements and context
Nonce allocation must be atomic and gap-free per (chain, account). Reuse the transactional
row-lock pattern from Store::allocate_address — the problem is the
same shape as muxed-id allocation.
Reconcile against eth_getTransactionCount with both latest and pending tags on startup and
periodically. These two disagree by design, and using the wrong one causes either gaps or
collisions — document which you use where and why.
Note the tension with AD-5. Replacement is a resubmit at the same nonce, which is exactly the
operation the submit-asymmetry rule forbids doing blindly. Replacement is safe only because the
nonce makes it mutually exclusive with the original. Make that reasoning explicit at the call
site so a future reader does not "fix" it by adding transport-level retries.
EIP-1559 gas: use eth_feeHistory for maxFeePerGas / maxPriorityFeePerGas. Cap the maximum
gas price — an unbounded escalation loop during a gas spike can drain the gas tank.
Handle nonce-gap recovery: if nonce n is permanently stuck, submit a self-transfer of 0 at
nonce n to unblock the queue.
Security: the gas price cap and the escalation policy bound how much a compromised or buggy
worker can spend. Treat them as security controls, not tuning parameters.
Suggested execution
Branch: feat/evm-nonce-and-tx-lifecycle
Implement changes
Migration 00NN_evm_tx_lifecycle.sql: an evm_transactions table with (chain, from_address,
nonce) unique, gas parameters, submission attempts, state machine (pending → submitted → mined → confirmed, plus replaced / dropped / failed), and the replacement chain.
Implement allocate_nonce(chain_id, address) under a row lock, with startup reconciliation
against on-chain counts.
Implement the lifecycle tracker: poll receipts for submitted transactions, detect drops
(submitted, absent from the mempool, nonce not advanced), and promote on confirmation depth.
Implement replacement: escalate gas by ≥ 12.5% (above the 10% minimum, for margin), resubmit at
the same nonce, and record the replacement chain so both hashes resolve to one logical transaction.
Add metrics and alerts: stuck transactions, replacement counts, gas spend per chain, nonce-gap
detection.
Test and commit
Concurrency test: N parallel allocations on one account produce N sequential, gap-free nonces.
feat(api): EVM nonce allocation and transaction lifecycle
Server-originated transactions (gas funding, sweeps) need strictly
sequential nonces: one stuck transaction halts every later one on the
account. Allocation reuses the row-lock pattern that keeps muxed ids
gap-free.
Adds replacement (same nonce, +12.5% gas) with an explicit price cap so
an escalation loop during a gas spike cannot drain the gas tank, plus
drop detection and 0-value self-transfer gap recovery.
Replacement resubmits at the same nonce, which is safe precisely
because the nonce makes it mutually exclusive with the original —
documented at the call site so it is not mistaken for a violation of
the submit-asymmetry rule.
Refs #226
Guidelines
Complex and easy to get subtly wrong. Write the state machine down before writing code, and put the
diagram in the PR description.
Depends on: #225. Blocks: #224.
Description
Stellar sequence numbers are handled by the client and surface as a
tx_bad_seqerror the userretries — see
explain_codeinsubmit.rs. EVM noncescannot be treated that way for server-originated transactions (gas funding and sweeps, #224), because
the server owns those accounts and must sequence them itself.
EVM adds three problems Stellar simply does not have:
n+1cannotmine until
ndoes. One stuck transaction halts every subsequent one on that account.only fix is replacement — resubmitting the same nonce with ≥ 10% higher gas.
simply does not exist any more, with no notification.
Build nonce management and a transaction lifecycle tracker for server-originated transactions.
Requirements and context
row-lock pattern from
Store::allocate_address— the problem is thesame shape as muxed-id allocation.
eth_getTransactionCountwith bothlatestandpendingtags on startup andperiodically. These two disagree by design, and using the wrong one causes either gaps or
collisions — document which you use where and why.
operation the submit-asymmetry rule forbids doing blindly. Replacement is safe only because the
nonce makes it mutually exclusive with the original. Make that reasoning explicit at the call
site so a future reader does not "fix" it by adding transport-level retries.
eth_feeHistoryformaxFeePerGas/maxPriorityFeePerGas. Cap the maximumgas price — an unbounded escalation loop during a gas spike can drain the gas tank.
nis permanently stuck, submit a self-transfer of 0 atnonce
nto unblock the queue.worker can spend. Treat them as security controls, not tuning parameters.
Suggested execution
Branch:
feat/evm-nonce-and-tx-lifecycleImplement changes
00NN_evm_tx_lifecycle.sql: anevm_transactionstable with (chain, from_address,nonce) unique, gas parameters, submission attempts, state machine (
pending→submitted→mined→confirmed, plusreplaced/dropped/failed), and the replacement chain.allocate_nonce(chain_id, address)under a row lock, with startup reconciliationagainst on-chain counts.
(submitted, absent from the mempool, nonce not advanced), and promote on confirmation depth.
the same nonce, and record the replacement chain so both hashes resolve to one logical transaction.
detection.
Test and commit
mines and the original is marked
replaced— notfailed, and not double-counted.latest/pendingdivergence and a pre-existing on-chainnonce ahead of the database.
docs/architecture.mdwith a state diagram.Example commit message
Guidelines
Complex and easy to get subtly wrong. Write the state machine down before writing code, and put the
diagram in the PR description.