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:

  1. Creates a PendingTx record containing the transaction and its list of missing inputs.
  2. Updates the bidirectional mappings: for each missing input, it inserts the transaction ID into pending_txs_by_inputs and populates pending_inputs_by_tx.
  3. Adjusts global pool counters for byte size, gas consumption, and transaction count.
  4. Pushes a TTL entry onto ttl_check using 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_tx iterates over each new output and delegates to new_known_input_from_output.
  • For each input, the method looks up pending_txs_by_inputs to find waiting transactions.
  • It removes the resolved input from the transaction’s missing_inputs list in pending_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_check while 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_inputs to remove stale dependency links.
  • It emits an ErrorInsertion notification 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.rs uses bidirectional hash maps (pending_txs_by_inputs and pending_inputs_by_tx) to track transactions waiting for unknown inputs.
  • Insertion via insert_transaction indexes transactions by their missing dependencies and schedules TTL expiration using a VecDeque timestamp queue.
  • Resolution occurs through new_known_tx, which promotes transactions to the main pool once all inputs become available on chain.
  • Expiration via expire_transactions removes stale entries based on configurable TTL, preventing memory leaks and notifying the system via ErrorInsertion events.
  • Observability is provided through dedicated metrics in txpool_metrics.rs and 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →