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) <noreply@anthropic.com>
This commit is contained in:
Hossein Zoda 2026-06-20 22:21:13 +00:00
parent 9b0a54c926
commit 62b8fb6436

View file

@ -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<i64, &'a IdoTxChainDBRecord>,
start_id: Option<i64>,
) -> Vec<&'a IdoTxChainDBRecord> {
let mut chain: Vec<&IdoTxChainDBRecord> = Vec::new();
let mut visited: HashSet<i64> = 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<Network>,
pool: &SqlitePool,
@ -2305,32 +2327,36 @@ async fn on_add_ido_tx(
) -> Result<()> {
debug!("IDO on_add_ido_tx: {}", blob_to_display_hex::<Txid>(&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<i64, &IdoTxChainDBRecord> =
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::<Txid>(&tx.compute_txid().to_blob())?, blob_to_display_hex::<Txid>(&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<Vec<u8>, 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"));
}