ido: replace txchain model with unified state-chain indexing

Replace index_block/index_mempool with a single index_txs that takes an
Option<blockhash> (Some for confirmed, None for mempool). Add
delete_entries(blockhash) for reorg undo and to drop stale mempool state
before applying confirmed blocks. Replace has_txchain_tx with
has_indexed_tx; chain-follow now keys on ido_state.next_output_index
instead of the removed tracker map.

Remove the debug-only txchain RPC endpoints and the debug config gating.
Keep txchain_head as a public RPC field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hossein Zoda 2026-06-22 19:46:23 +00:00
parent e2f72e608e
commit 0831834d8f
5 changed files with 773 additions and 930 deletions

View file

@ -88,6 +88,12 @@ impl BlockUndoer for StoreBlockUndoer {
{
was_indexed = true;
}
if db::ido::delete_entries(&self.db.ido_w, Some(&blockheader.block_hash()))
.await?
> 0
{
was_indexed = true;
}
if was_indexed {
config_set(

File diff suppressed because one or more lines are too long

View file

@ -26,6 +26,7 @@ use crate::{
crc20::index_crc20,
db::{
self,
blob::ToBlob,
cauldron::{
config::{config_get, config_set},
header::{db_get_header, store_headers},
@ -189,17 +190,10 @@ pub async fn update_mempool(
// ido updates
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 recorded in an ido's state chain
let mut ido_to_add = Vec::new();
for txid in ido_txs {
if !db::ido::has_txchain_tx(&db.ido_w, &txid).await? {
if !db::ido::has_indexed_tx(&db.ido_w, &txid.to_blob()).await? {
ido_to_add.push(txid);
}
}
@ -220,9 +214,9 @@ pub async fn update_mempool(
})
.await?;
// an ido txchain can have several unconfirmed txs in flight; index parents first
// an ido chain can have several unconfirmed txs in flight; index parents first
let txs_to_add = ttor_sorted_kahn(txs_to_add);
db::ido::index_mempool(network, &db.ido_w, &txs_to_add, tip_height).await?;
db::ido::index_txs(network, &db.ido_w, &txs_to_add, None).await?;
Ok(())
}
@ -305,6 +299,14 @@ pub async fn index_blocks(
}
}
// Before applying confirmed blocks, drop any mempool-indexed IDO state
// (blockhash IS NULL). A mempool tx that never confirmed — e.g. one replaced
// by a different on-chain tx — must not leave a stale snapshot that wins
// MAX(seq) or resurfaces after the reorg undo above. Confirmed blocks
// re-create rows for the txs they contain, and the next mempool pass
// (update_mempool) rebuilds the rest.
db::ido::delete_entries(&db.ido_w, None).await?;
let db_cpy = db.clone();
let chain_cpy = chain.clone();
let client_cpy = client.clone();
@ -486,15 +488,7 @@ pub async fn index_blocks(
)
.await?;
db::ido::index_block(
network,
&db.ido_w,
&sorted_txs,
&blockhash,
mtp as i64,
block_height as i64,
)
.await?;
db::ido::index_txs(network, &db.ido_w, &sorted_txs, Some(&blockhash)).await?;
let autheader_updates = if bcmr_enabled {
let updates = index_bcmr(&db.bcmr_w, &blockhash, sorted_txs).await?;

View file

@ -380,9 +380,6 @@ async fn launch() -> _ {
config
};
// Debug-only RPC endpoints (e.g. txchain inspection) are gated behind this flag.
let debug_endpoints = config.debug;
let (
dbpool,
bcmrdownloader,
@ -570,20 +567,12 @@ async fn launch() -> _ {
}
// Always-on IDO endpoints.
let mut ido_routes = routes![
let ido_routes = routes![
rpc::ido::list_idos,
rpc::ido::get_ido_by_id,
rpc::ido::get_ido_by_offering_token,
rpc::ido::list_ido_entries,
rpc::ido::get_txchain_tx,
];
// Debug-only IDO endpoints, mounted only when `debug = true` in the config.
if debug_endpoints {
ido_routes.extend(routes![
rpc::ido::list_ido_txchain,
rpc::ido::list_txchain_tracker_map,
]);
}
rocket::build()
.attach(signal::ShutdownFairing)

View file

@ -20,9 +20,6 @@ const MAX_OWNER_NFTHASH_FILTERS: usize = 20;
/// Length in bytes of an owner_nfthash.
const OWNER_NFTHASH_LEN: usize = 32;
const DEBUG_DEFAULT_LIMIT: i64 = 100;
const DEBUG_MAX_LIMIT: i64 = 10_000;
/// List IDOs with optional filters and pagination.
///
/// Filters:
@ -190,51 +187,6 @@ pub async fn list_ido_entries(
Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE))
}
/// Get the raw transaction hex for a txchain item by its txid.
/// Returns `{"tx": "<hex>"}` or `{"tx": null}` if not found.
#[get("/txchain/<txid>/tx")]
pub async fn get_txchain_tx(txid: &str, db: &State<DB>) -> CachedApiResult<Value> {
let txid_blob = display_hex_to_blob::<Txid>(txid).map_err(|e| {
bad_request(
ApiErrorCode::InvalidParameters,
&format!("Invalid txid hex: {e}"),
)
})?;
let tx_hex = crate::db::ido::get_txchain_tx_hex(&db.ido_r, &txid_blob)
.await
.map_err(db_error)?;
Ok(cached_ok(serde_json::json!({ "tx": tx_hex }), CACHE_NONE))
}
/// [Debug] List the txchain for an IDO, sorted oldest-first (head is last).
///
/// - `id`: the IDO's public id (preinit txid, 64-char hex)
///
/// Pagination: `offset` (default 0), `limit` (default 100, max 10000)
#[get("/<id>/txchain?<offset>&<limit>")]
pub async fn list_ido_txchain(
id: &str,
offset: Option<i64>,
limit: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<Value> {
let offset = offset.unwrap_or(0).max(0);
let limit = limit
.unwrap_or(DEBUG_DEFAULT_LIMIT)
.clamp(1, DEBUG_MAX_LIMIT);
let (internal_id, preinit_txid_hex) = resolve_ido(id, &db.ido_r).await?;
let items =
crate::db::ido::list_ido_txchain(&db.ido_r, internal_id, &preinit_txid_hex, offset, limit)
.await
.map_err(db_error)?;
Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE))
}
async fn resolve_ido(
id: &str,
pool: &sqlx::SqlitePool,
@ -260,24 +212,3 @@ async fn resolve_ido(
Ok((internal_id, preinit_txid_hex))
}
/// [Debug] List all entries in the txchain tracker map.
///
/// Pagination: `offset` (default 0), `limit` (default 100, max 10000)
#[get("/txchain-tracker?<offset>&<limit>")]
pub async fn list_txchain_tracker_map(
offset: Option<i64>,
limit: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<Value> {
let offset = offset.unwrap_or(0).max(0);
let limit = limit
.unwrap_or(DEBUG_DEFAULT_LIMIT)
.clamp(1, DEBUG_MAX_LIMIT);
let items = crate::db::ido::list_txchain_tracker_map(&db.ido_r, offset, limit)
.await
.map_err(db_error)?;
Ok(cached_ok(serde_json::to_value(items).unwrap(), CACHE_NONE))
}