ido: garbage collect tracker entries of mempool txs that never confirm

Stamp mempool tracker entries with the negative of the highest known
block height instead of a fixed -1 sentinel, and trim on abs(height) so
entries whose tx is invalidated before confirming (e.g. by a double
spend) age out after TRACKER_MAX_BLOCK_DEPTH like confirmed ones. The
height is still replaced with the real one once the tx confirms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hossein Zoda 2026-06-04 16:58:13 +00:00
parent 7f46b1b4b7
commit b1df63c6c4
2 changed files with 21 additions and 8 deletions

View file

@ -903,8 +903,11 @@ async fn txchain_trim_tracker(
pool: &SqlitePool, pool: &SqlitePool,
current_block_height: i64, current_block_height: i64,
) -> Result<()> { ) -> Result<()> {
// A negative height marks a mempool entry stamped with the negative of the
// highest known block height when it was indexed (see index_mempool), so
// abs(height) is the entry's reference height in both cases.
sqlx::query( sqlx::query(
"DELETE FROM ido_txchain_tracker_map AS t0 WHERE t0.height < ? AND t0.height >= 0 AND NOT EXISTS (SELECT 1 FROM ido t1 WHERE t1.txchain_head = t0.txchain_id)", "DELETE FROM ido_txchain_tracker_map AS t0 WHERE abs(t0.height) < ? AND NOT EXISTS (SELECT 1 FROM ido t1 WHERE t1.txchain_head = t0.txchain_id)",
) )
.bind(current_block_height - TRACKER_MAX_BLOCK_DEPTH) .bind(current_block_height - TRACKER_MAX_BLOCK_DEPTH)
.execute(pool) .execute(pool)
@ -2288,17 +2291,20 @@ pub async fn index_block(
Ok(()) Ok(())
} }
// Sentinel block height for txs indexed from the mempool. txchain_trim_tracker
// never trims tracker entries with a negative height; the height is replaced
// with the real one once the tx confirms in a block.
const MEMPOOL_BLOCK_HEIGHT: i64 = -1;
pub async fn index_mempool( pub async fn index_mempool(
network: Option<Network>, network: Option<Network>,
pool: &SqlitePool, pool: &SqlitePool,
sorted_txs: &[Transaction], sorted_txs: &[Transaction],
highest_known_block_height: u64,
) -> Result<()> { ) -> Result<()> {
index_txs(network, pool, sorted_txs, MEMPOOL_BLOCK_HEIGHT) // Txs indexed from the mempool are stamped with the negative of the
// highest known block height, so txchain_trim_tracker can garbage collect
// entries that never confirm (e.g. invalidated by a double spend) once
// they pass TRACKER_MAX_BLOCK_DEPTH. The height is replaced with the real
// (positive) one once the tx confirms in a block. max(1) avoids storing 0,
// which would be indistinguishable from a confirmed height.
let mempool_height = -(highest_known_block_height.max(1) as i64);
index_txs(network, pool, sorted_txs, mempool_height)
.await .await
} }

View file

@ -193,6 +193,13 @@ pub async fn update_mempool(
// ido updates // ido updates
let ido_electrum = electrum.clone(); let ido_electrum = electrum.clone();
// highest known block height; mempool tracker entries are stamped with its
// negative so ido's txchain_trim_tracker can age them out (see index_mempool)
let tip_electrum = electrum.clone();
let (_, tip_height) =
tokio::task::spawn_blocking(move || electrum_get_tip(&tip_electrum.lock().unwrap()))
.await??;
// filter txs already in an ido txchain // filter txs already in an ido txchain
let mut ido_to_add = Vec::new(); let mut ido_to_add = Vec::new();
for txid in ido_txs { for txid in ido_txs {
@ -219,7 +226,7 @@ pub async fn update_mempool(
// an ido txchain can have several unconfirmed txs in flight; index parents first // an ido txchain can have several unconfirmed txs in flight; index parents first
let txs_to_add = ttor_sorted_kahn(txs_to_add); let txs_to_add = ttor_sorted_kahn(txs_to_add);
db::ido::index_mempool(network, &db.ido_w, &txs_to_add).await?; db::ido::index_mempool(network, &db.ido_w, &txs_to_add, tip_height).await?;
Ok(()) Ok(())
} }