How Txpool v2 Manages Pending Transactions in Fuel Core
Fuel Core’s txpool v2 uses a dedicated PendingPool module to store transactions with unresolved inputs, tracking dependencies through bidirectional hash maps and automatically promoting them to the main pool when their required UTXOs or contracts appear on chain.
Fuel Core, the Rust-based full node powering the Fuel blockchain, separates transaction validation into distinct lifecycle stages to maximize throughput. When the txpool v2 receives transactions that reference inputs not yet existing in the current chain state—such as UTXOs from pending blocks or newly deployed contracts—it cannot immediately verify their validity. Rather than discarding these transactions, the implementation holds them in a pending pool, indexed by their missing dependencies, and resolves them asynchronously as the blockchain evolves.
Core Data Structures in PendingPool
The pending pool implementation resides in crates/services/txpool_v2/src/pending_pool.rs, where the PendingPool struct (defined at line 50) maintains three synchronized storage mechanisms to track orphan transactions efficiently:
pending_txs_by_inputs: HashMap<MissingInput, HashSet<TxId>>— Maps each missing input (UTXO ID or contract) to the set of transactions waiting for it. This allows O(1) lookup when a new input becomes available.pending_inputs_by_tx: HashMap<TxId, PendingTx>— Stores the transaction object alongside its remaining list of missing inputs, enabling quick updates when partial resolution occurs.ttl_check: VecDeque<(SystemTime, TxId)>— A time-ordered queue that pairs each transaction with its expiration timestamp, supporting efficient background cleanup.
This dual-map architecture ensures that the pool can locate all transactions dependent on a specific input instantly, while also tracking per-transaction metadata and expiration deadlines.
Inserting Transactions with Missing Inputs
When the txpool receives a transaction whose inputs are not fully known, it invokes insert_transaction (line 12 in pending_pool.rs). This method performs the following operations:
- Creates a
PendingTxrecord containing the transaction and its list of missing inputs. - Updates the bidirectional mappings: for each missing input, it inserts the transaction ID into
pending_txs_by_inputsand populatespending_inputs_by_tx. - Adjusts global pool counters for byte size, gas consumption, and transaction count.
- Pushes a TTL entry onto
ttl_checkusing the configured expiration time (SystemTime::now() + ttl).
The insertion source—whether from RPC, P2P gossip, or block import—is preserved to ensure proper error propagation if the transaction expires before resolution.
Resolving Pending Transactions When Inputs Arrive
As the Fuel node imports new blocks or processes other transactions, previously missing UTXOs and contracts become available. The pending pool resolves these dependencies through new_known_tx (line 30), which accepts an iterator of (UtxoId, &Output) pairs representing newly created outputs.
The resolution workflow proceeds as follows:
new_known_txiterates over each new output and delegates tonew_known_input_from_output.- For each input, the method looks up
pending_txs_by_inputsto find waiting transactions. - It removes the resolved input from the transaction’s
missing_inputslist inpending_inputs_by_tx. - When a transaction’s missing input list becomes empty, the implementation removes it entirely from the pending pool and returns it to the caller for insertion into the main transaction pool.
This incremental resolution ensures that transactions are only promoted once all their dependencies are satisfied, preventing invalid transactions from entering the executable pool.
TTL-Based Expiration and Garbage Collection
To prevent unbounded memory growth from transactions waiting indefinitely for inputs that may never materialize, txpool v2 implements strict TTL-based expiration. The expire_transactions method (line 84) runs periodically as a background task:
- It pops entries from the back of
ttl_checkwhile their timestamp is ≤ current system time. - For each expired transaction, it decrements the pool’s size, gas, and transaction counters.
- It cleans the corresponding entries from
pending_txs_by_inputsto remove stale dependency links. - It emits an
ErrorInsertionnotification containing the original missing input, allowing upstream components to notify submitters or log the failure.
The TTL duration is configurable via TxPoolConfig in crates/services/txpool_v2/src/config.rs, specifically through the pending_pool_ttl parameter read at node startup.
Configuration and Monitoring
Operators can monitor pending pool health through metrics exposed in crates/metrics/src/txpool_metrics.rs. Key gauges include number_of_transactions_pending_verification, which reflects the current count in pending_inputs_by_tx, and associated byte/gas totals.
Integration tests in crates/services/txpool_v2/src/pending_pool.rs (line 103) verify the complete lifecycle: inserting a dependent transaction, supplying the missing UTXO via new_known_tx, and asserting that the pending pool empties as the transaction graduates to the main pool. Additional end-to-end coverage exists in tests/tests/tx/txpool.rs.
Summary
- PendingPool in
crates/services/txpool_v2/src/pending_pool.rsuses bidirectional hash maps (pending_txs_by_inputsandpending_inputs_by_tx) to track transactions waiting for unknown inputs. - Insertion via
insert_transactionindexes transactions by their missing dependencies and schedules TTL expiration using aVecDequetimestamp queue. - Resolution occurs through
new_known_tx, which promotes transactions to the main pool once all inputs become available on chain. - Expiration via
expire_transactionsremoves stale entries based on configurable TTL, preventing memory leaks and notifying the system viaErrorInsertionevents. - Observability is provided through dedicated metrics in
txpool_metrics.rsand validated by unit tests at line 103 of the pending pool module.
Frequently Asked Questions
What happens to a transaction if its required UTXO never appears?
If the missing input does not materialize within the configured TTL window, expire_transactions removes the transaction from the pending pool and emits an ErrorInsertion notification. The transaction is dropped without entering the main pool, and the submitter can be notified that their inputs were unavailable.
How does txpool v2 handle multiple missing inputs for a single transaction?
The PendingTx struct stores the complete list of missing inputs. Each input is registered in pending_txs_by_inputs, and the transaction remains pending until new_known_input_from_output removes the final dependency. Only when the list empties does the transaction return for main pool insertion.
Where is the pending pool TTL configured?
The TTL is defined in TxPoolConfig within crates/services/txpool_v2/src/config.rs via the pending_pool_ttl field. This duration is read at node startup and passed to the PendingPool constructor, determining how long transactions may wait for unresolved inputs before automatic expiration.
Can pending transactions consume node resources indefinitely?
No. The ttl_check queue ensures that memory, gas, and byte counters are strictly bounded. Even if inputs never arrive, the periodic expire_transactions call purges old entries and updates metrics, preventing resource exhaustion regardless of network conditions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →