From 62b8fb6436272cca373cf4d7d6ca3896937a2b3c Mon Sep 17 00:00:00 2001 From: Hossein Zoda Date: Sat, 20 Jun 2026 22:21:13 +0000 Subject: [PATCH] ido: only update block_height when tx is already in the txchain on_add_ido_tx rebuilt the whole chain whenever the input tx was not the txchain head. A mempool tx confirming after the chain advanced past it would needlessly trigger a full rebuild. Reconstruct the current chain (txchain_head .. txchain_entrypoint) and, if the tx is already part of it, only update its block height. Extract the shared chain-walking logic into reconstruct_txchain, reused by both the membership check and the rebuild branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/db/ido/mod.rs | 60 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/src/db/ido/mod.rs b/src/db/ido/mod.rs index 81ef66f..ce29e52 100644 --- a/src/db/ido/mod.rs +++ b/src/db/ido/mod.rs @@ -12,7 +12,7 @@ use serde_with::{serde_as, DisplayFromStr}; use bitcoincash::blockdata::transaction::Transaction; use bitcoincash::{Txid, BlockHash, TokenID, Network}; use std::sync::LazyLock; -use std::collections::{HashMap}; +use std::collections::{HashMap, HashSet}; use sqlx::{Row, SqlitePool}; use crate::db::blob::{ToBlob, blob_to_display_hex}; use anyhow::Error; @@ -2295,6 +2295,28 @@ async fn update_ido_txchain_tracker_block_height( Ok(()) } +/// Reconstruct the ordered txchain (entrypoint .. start) by following prev_id +/// back from `start_id` through `items_by_id`. The returned vec is ordered from +/// the oldest item to `start_id`; it stops at the entrypoint (the item with no +/// prev_id) and is guarded against a malformed prev_id cycle. The chain is only +/// well-formed if its first item is the ido's entrypoint. +fn reconstruct_txchain<'a>( + items_by_id: &HashMap, + start_id: Option, +) -> Vec<&'a IdoTxChainDBRecord> { + let mut chain: Vec<&IdoTxChainDBRecord> = Vec::new(); + let mut visited: HashSet = HashSet::new(); + let mut cursor = start_id.and_then(|id| items_by_id.get(&id).copied()); + while let Some(item) = cursor { + if !visited.insert(item.id) { + break; // guard against a malformed prev_id cycle + } + chain.insert(0, item); + cursor = item.prev_id.and_then(|prev_id| items_by_id.get(&prev_id).copied()); + } + chain +} + async fn on_add_ido_tx( network: Option, pool: &SqlitePool, @@ -2305,32 +2327,36 @@ async fn on_add_ido_tx( ) -> Result<()> { debug!("IDO on_add_ido_tx: {}", blob_to_display_hex::(&tx.compute_txid().to_blob())?); let mut dbtx = pool.begin().await?; - let current_item = get_txchain_item_by_txid(pool, &tx.compute_txid()) - .await?; - if current_item.is_some() && Some(current_item.unwrap().id) == ido.txchain_head { - // already added, only update block height + let items = get_ido_txchain_list(pool, ido.internal_id) + .await?; + // Reconstruct this ido's current txchain — exactly txchain_head back to + // txchain_entrypoint, following prev_id — and check whether this tx is part + // of it. A mempool tx that confirms after the chain has advanced past it is + // still part of the chain even though it is no longer the head; it only + // needs its block height updated, not a full rebuild. + let items_by_id: HashMap = + items.iter().map(|item| (item.id, item)).collect(); + let chain = reconstruct_txchain(&items_by_id, ido.txchain_head); + // the chain is only valid if it runs the whole way from the head down to + // the entrypoint (preinit) — nothing more, nothing less + let is_complete_chain = ido.txchain_entrypoint.is_some() + && chain.first().map(|item| item.id) == ido.txchain_entrypoint; + let tx_txid = tx.compute_txid().to_blob(); + let tx_in_current_chain = + is_complete_chain && chain.iter().any(|item| item.txid == tx_txid); + if tx_in_current_chain { + // already part of the current txchain, only update block height update_ido_txchain_tracker_block_height(&mut dbtx, tx, block_height) .await?; } else if prev_txchain_item.id != ido.txchain_head.unwrap_or(0) { debug!("IDO rebuild: {}, prev: {}", blob_to_display_hex::(&tx.compute_txid().to_blob())?, blob_to_display_hex::(&prev_txchain_item.txid)?); // rebuild the state, txchain is broken, recreate the chain - let items = get_ido_txchain_list(pool, ido.internal_id) - .await?; let mut items_tx_map: HashMap, Transaction> = HashMap::new(); for item in &items { let tx_deser: Transaction = bitcoincash::consensus::deserialize(&item.tx).map_err(|e| anyhow::anyhow!("failed to deserialize tx: {e}"))?; items_tx_map.insert(item.txid.clone(), tx_deser); } - let mut new_chain: Vec<&IdoTxChainDBRecord> = Vec::new(); - let mut items_copy: Vec<&IdoTxChainDBRecord> = items.iter().rev().collect(); - let mut current_item = prev_txchain_item; - new_chain.insert(0, current_item); - while current_item.prev_id.is_some() { - let prev_id = current_item.prev_id.unwrap(); - let found_index = items_copy.iter().position(|a| a.id == prev_id).ok_or_else(|| anyhow::anyhow!("txchain prev item not found!"))?; - current_item = items_copy.remove(found_index); - new_chain.insert(0, current_item); - } + let new_chain = reconstruct_txchain(&items_by_id, Some(prev_txchain_item.id)); if new_chain.get(0).ok_or_else(|| anyhow::anyhow!("txchain is empty"))?.txid != ido.preinit_txid { return Err(anyhow::anyhow!("txchain entrypoint does not match preinit_txid")); }