docs/core/TX_MANAGER_ARCHITECTURE.md
evm.txes.stateunstarted
|
|
v
in_progress (only one per key)
|
|
v v
fatal_error unconfirmed
| ^
| |
v |
confirmed
eth_tx_attempts.statein_progress
| ^
| |
v |
broadcast
Key:
⚫️ - has never been broadcast to the network
🟠 - may or may not have been broadcast to the network
🔵 - has definitely been broadcast to the network
EB - EthBroadcaster
EC - EthConfirmer
evm.txes has five possible states:
unstartedin_progressfatal_errorunconfirmedconfirmedeth_tx_attempts has two possible states:
in_progressbroadcastAn attempt may have 0 or more eth_receipts indicating that the transaction has been mined into a block. This block may or may not exist as part of the canonical longest chain.
BulletproofTxManager is split into three components, each of which has a clearly delineated set of responsibilities.
Conceptually, EthTx defines the transaction.
EthTx is responsible for generating the transaction criteria and inserting the initial unstarted row into the evm.txes table.
EthTx guarantees that the transaction is defined with the following criteria:
Only one transaction may be created per EthTx task.
EthTx should wait until it's transaction confirms before marking the task as completed.
Conceptually, EthBroadcaster assigns a nonce to a transaction and ensures that it is valid. It alone maintains the next usable sequence for a transaction.
EthBroadcaster monitors evm.txes for transactions that need to be broadcast, assigns nonces and ensures that at least one eth node somewhere has placed the transaction into its mempool.
It does not guarantee eventual confirmation!
A whole host of other things can subsequently go wrong such as transactions being evicted from the mempool, eth nodes crashing, netsplits between eth nodes, chain re-orgs etc. Responsibility for ensuring eventual inclusion into the longest chain falls on the shoulders of EthConfirmer.
EthBroadcaster makes the following guarantees:
evm.txes (scoped to key).evm.txes from unstarted to either fatal_error or unconfirmed.fatal_error then the nonce is unassigned, and it is impossible that this transaction could ever be mined into a block.unconfirmed then a saved eth_transaction_attempt exists.unconfirmed then an eth node somewhere has accepted this transaction into its mempool at least once.EthConfirmer must serialize access on a per-key basis since nonce assignment needs to be tightly controlled. Multiple keys can however be processed in parallel. Serialization is enforced with an advisory lock scoped to the key.
Conceptually, EthConfirmer adjusts the gas price as necessary to get a transaction mined into a block on the longest chain.
EthConfirmer listens to new heads and performs four separate tasks in sequence every time we become aware of a longer chain.
When we receive a block we can be sure that any currently unconfirmed transactions were broadcast before this block was received, so we set broadcast_before_block_num on all transaction attempts made since we saw the last block.
It is important to know how long a transaction has been waiting for inclusion, so we can know for how many blocks a transaction has been waiting for inclusion in order to decide if we need to bump gas.
Find all unconfirmed transactions and ask the eth node for a receipt. If there is a receipt, we save it and move this transaction into confirmed state.
Find all unconfirmed transactions where all attempts have remained unconfirmed for more than ETH_GAS_BUMP_THRESHOLD blocks. Create a new eth_transaction_attempt for each, with a higher gas price.
Find all transactions confirmed within the past ETH_FINALITY_DEPTH blocks and verify that they have at least one receipt in the current longest chain. If any do not, then rebroadcast those transactions.
EthConfirmer makes the following guarantees:
ETH_FINALITY_DEPTH blocks.ETH_FINALITY_DEPTH blocks but it most likely will not be the transaction in our database.Note that since checking for inclusion in the longest chain can now be done cheaply, without any calls to the eth node, ETH_FINALITY_DEPTH can be set to something quite large without penalty (e.g. 50 or 100).
EthBroadcaster runs are designed to be serialized. Running it concurrently with itself probably can't get the data into an inconsistent state, but it might hit database conflicts or double-send transactions. Serialization is enforced with an advisory lock.
The design of EthConfirmer relies on an unbroken chain of heads in our database. If there is a break in the chain of heads, our re-org protection is limited to this break.
For example if we have heads at heights:
1
2
4
Then a reorg that happened at block height 3 or above will not be detected and any transactions mined in those blocks may be left erroneously marked as confirmed.
Currently, the design of the head tracker opens us up to gaps in the head sequence. This can occur in several scenarios:
For this reason, I propose that follow-up work should be undertaken to ensure that the head tracker has some facility for backfilling heads up toETH_FINALITY_DEPTH.