ido: index unconfirmed txs from the mempool
- electrum mempool fetch gains an ido filter: state machine spends (IDO_SIGNATURE in scriptsig) union preinit announcements (IDO_PREINIT_ANNOUNCEMENT_SIGNATURE in scriptpubkey) - split tx scanning out of index_block into index_txs and add index_mempool, which indexes with a -1 sentinel height; txchain_trim_tracker leaves negative heights alone and the real height replaces the sentinel once the tx confirms in a block - block indexing now skips re-creating an ido already seen in the mempool, only updating its tracker height - mempool txs are kahn-sorted so txchain parents index before children Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e14fbd4f1e
commit
7f46b1b4b7
4 changed files with 103 additions and 9 deletions
|
|
@ -82,7 +82,9 @@ const CHIPNET_PLATFORM_FEE_NFTH: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decod
|
||||||
// TODO:: set a mock nfthash for mainnet
|
// TODO:: set a mock nfthash for mainnet
|
||||||
const MAINNET_PLATFORM_FEE_NFTH: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("").unwrap());
|
const MAINNET_PLATFORM_FEE_NFTH: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("").unwrap());
|
||||||
|
|
||||||
const IDO_SIGNATURE: &[u8] = &[
|
// (OP_PUSH18 "CauldronIdo-2026Q2" OP_DROP), found at the start of every ido
|
||||||
|
// state machine redeem script.
|
||||||
|
pub const IDO_SIGNATURE: &[u8] = &[
|
||||||
0x12, 0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49,
|
0x12, 0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49,
|
||||||
0x64, 0x6f, 0x2d, 0x32, 0x30, 0x32, 0x36, 0x51, 0x32, 0x75,
|
0x64, 0x6f, 0x2d, 0x32, 0x30, 0x32, 0x36, 0x51, 0x32, 0x75,
|
||||||
];
|
];
|
||||||
|
|
@ -910,7 +912,9 @@ async fn txchain_trim_tracker(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
const IDO_PREINIT_ANNOUNCEMENT_SIGNATURE: &[u8; 15] = &[
|
// Prefix of the announcement OP_RETURN carried in the last output of a preinit
|
||||||
|
// broadcast tx.
|
||||||
|
pub const IDO_PREINIT_ANNOUNCEMENT_SIGNATURE: &[u8; 15] = &[
|
||||||
0x6a, 0x4c, 0xbb, // OP_RETURN OP_PUSHDATA1 (187)
|
0x6a, 0x4c, 0xbb, // OP_RETURN OP_PUSHDATA1 (187)
|
||||||
0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49, 0x64, 0x6f, 0x30, // CauldronIdo0
|
0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49, 0x64, 0x6f, 0x30, // CauldronIdo0
|
||||||
];
|
];
|
||||||
|
|
@ -2213,17 +2217,23 @@ fn is_ido_sig_in_tx_inputs(tx: &Transaction) -> bool {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn index_block(
|
async fn index_txs(
|
||||||
network: Option<Network>,
|
network: Option<Network>,
|
||||||
pool: &SqlitePool,
|
pool: &SqlitePool,
|
||||||
sorted_txs: &[Transaction],
|
sorted_txs: &[Transaction],
|
||||||
_blockhash: &BlockHash,
|
|
||||||
_mtp: i64,
|
|
||||||
block_height: i64,
|
block_height: i64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for tx in sorted_txs {
|
for tx in sorted_txs {
|
||||||
// detect a new ido
|
// detect a new ido
|
||||||
if is_preinit_broadcast(tx) {
|
if is_preinit_broadcast(tx) {
|
||||||
|
if lookup_internal_id_by_preinit_txid(pool, &tx.txid().to_blob()).await?.is_some() {
|
||||||
|
// already created (eg. indexed from the mempool), only update block height
|
||||||
|
let mut dbtx = pool.begin().await?;
|
||||||
|
update_ido_txchain_tracker_block_height(&mut dbtx, tx, block_height)
|
||||||
|
.await?;
|
||||||
|
dbtx.commit().await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match on_create_ido(network, pool, tx, block_height)
|
match on_create_ido(network, pool, tx, block_height)
|
||||||
.await {
|
.await {
|
||||||
Err(err) => info!("failed to detect an ido, or an invalid ido detected, txid: {}, {}", hex::encode(tx.txid().to_blob()), err),
|
Err(err) => info!("failed to detect an ido, or an invalid ido detected, txid: {}, {}", hex::encode(tx.txid().to_blob()), err),
|
||||||
|
|
@ -2260,11 +2270,46 @@ pub async fn index_block(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn index_block(
|
||||||
|
network: Option<Network>,
|
||||||
|
pool: &SqlitePool,
|
||||||
|
sorted_txs: &[Transaction],
|
||||||
|
_blockhash: &BlockHash,
|
||||||
|
_mtp: i64,
|
||||||
|
block_height: i64,
|
||||||
|
) -> Result<()> {
|
||||||
|
index_txs(network, pool, sorted_txs, block_height)
|
||||||
|
.await?;
|
||||||
txchain_trim_tracker(pool, block_height)
|
txchain_trim_tracker(pool, block_height)
|
||||||
.await?;
|
.await?;
|
||||||
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(
|
||||||
|
network: Option<Network>,
|
||||||
|
pool: &SqlitePool,
|
||||||
|
sorted_txs: &[Transaction],
|
||||||
|
) -> Result<()> {
|
||||||
|
index_txs(network, pool, sorted_txs, MEMPOOL_BLOCK_HEIGHT)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the tx is already part of an indexed ido txchain.
|
||||||
|
pub async fn has_txchain_tx(
|
||||||
|
pool: &SqlitePool,
|
||||||
|
txid: &Txid,
|
||||||
|
) -> Result<bool> {
|
||||||
|
Ok(get_txchain_item_by_txid(pool, txid).await?.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Public RPC types ────────────────────────────────────────────────────────
|
// ─── Public RPC types ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
|
|
@ -2778,6 +2823,7 @@ mod querytests {
|
||||||
idoCategory: vec![0xBB; 32],
|
idoCategory: vec![0xBB; 32],
|
||||||
nextTxSetterFlag: BigInt::from(0i64),
|
nextTxSetterFlag: BigInt::from(0i64),
|
||||||
oTokenGenerated: BigInt::from(1i64),
|
oTokenGenerated: BigInt::from(1i64),
|
||||||
|
initiatorCreated: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ use riftenlabs_defi::{
|
||||||
};
|
};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use crate::db::ido::{IDO_PREINIT_ANNOUNCEMENT_SIGNATURE, IDO_SIGNATURE};
|
||||||
|
|
||||||
/// Fetch blockchain tip from electrum server
|
/// Fetch blockchain tip from electrum server
|
||||||
pub fn electrum_get_tip(client: &Client) -> Result<(BlockHeader, u64)> {
|
pub fn electrum_get_tip(client: &Client) -> Result<(BlockHeader, u64)> {
|
||||||
let tip: Value =
|
let tip: Value =
|
||||||
|
|
@ -39,7 +41,7 @@ pub fn electrum_get_tip(client: &Client) -> Result<(BlockHeader, u64)> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch defi (cauldron + tokentoken) and oracle mempool transactions
|
/// Fetch defi (cauldron + tokentoken) and oracle mempool transactions
|
||||||
pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet<Txid>, HashSet<Txid>)> {
|
pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet<Txid>, HashSet<Txid>, HashSet<Txid>)> {
|
||||||
let cauldron_filter = json!({
|
let cauldron_filter = json!({
|
||||||
"scriptsig": hex::encode(&V2_CONTRACT_TEMPLATE[(V2_CONTRACT_TEMPLATE.len() - 43)..]), // cauldron spends
|
"scriptsig": hex::encode(&V2_CONTRACT_TEMPLATE[(V2_CONTRACT_TEMPLATE.len() - 43)..]), // cauldron spends
|
||||||
"scriptpubkey": hex::encode([0x6a /* op_return */, 0x06 /* push */, b'S', b'U', b'M', b'M', b'O', b'N']), // new pools (potentially)
|
"scriptpubkey": hex::encode([0x6a /* op_return */, 0x06 /* push */, b'S', b'U', b'M', b'M', b'O', b'N']), // new pools (potentially)
|
||||||
|
|
@ -72,6 +74,12 @@ pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet<Txid>, HashSet
|
||||||
"scriptsig": hex::encode(DELPHI_V2_REDEEM_SCRIPT_BODY),
|
"scriptsig": hex::encode(DELPHI_V2_REDEEM_SCRIPT_BODY),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let ido_filter = json!({
|
||||||
|
"scriptsig": hex::encode(IDO_SIGNATURE), // ido state machine spends
|
||||||
|
"scriptpubkey": hex::encode(IDO_PREINIT_ANNOUNCEMENT_SIGNATURE), // preinit announcements (potentially)
|
||||||
|
"operation": "union"
|
||||||
|
});
|
||||||
|
|
||||||
let fetch_txs = |filter: Value| -> Result<HashSet<Txid>> {
|
let fetch_txs = |filter: Value| -> Result<HashSet<Txid>> {
|
||||||
let response = client.raw_call("mempool.get", [Param::Value(filter)])?;
|
let response = client.raw_call("mempool.get", [Param::Value(filter)])?;
|
||||||
|
|
||||||
|
|
@ -104,8 +112,9 @@ pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet<Txid>, HashSet
|
||||||
defi_txs.extend(fetch_txs(tokentoken_filter)?);
|
defi_txs.extend(fetch_txs(tokentoken_filter)?);
|
||||||
let mut oracle_txs = fetch_txs(oracle_v1_filter)?;
|
let mut oracle_txs = fetch_txs(oracle_v1_filter)?;
|
||||||
oracle_txs.extend(fetch_txs(oracle_v2_filter)?);
|
oracle_txs.extend(fetch_txs(oracle_v2_filter)?);
|
||||||
|
let ido_txs = fetch_txs(ido_filter)?;
|
||||||
|
|
||||||
Ok((defi_txs, oracle_txs))
|
Ok((defi_txs, oracle_txs, ido_txs))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch blockchain tip from electrum server
|
/// Fetch blockchain tip from electrum server
|
||||||
|
|
|
||||||
41
src/index.rs
41
src/index.rs
|
|
@ -67,12 +67,20 @@ fn moria_token_ids(network: Option<Network>) -> MoriaTokenIds {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_mempool(db: &DB, electrum: Arc<Mutex<Client>>) -> Result<()> {
|
pub async fn update_mempool(
|
||||||
|
db: &DB,
|
||||||
|
electrum: Arc<Mutex<Client>>,
|
||||||
|
network: Option<Network>,
|
||||||
|
) -> Result<()> {
|
||||||
let our_mempool_txs: HashSet<Txid> =
|
let our_mempool_txs: HashSet<Txid> =
|
||||||
db::cauldron::mempool::load_mempool(&db.cauldron_w).await?;
|
db::cauldron::mempool::load_mempool(&db.cauldron_w).await?;
|
||||||
|
|
||||||
let electrum_clone = electrum.clone();
|
let electrum_clone = electrum.clone();
|
||||||
|
<<<<<<< HEAD
|
||||||
let (defi_txs, oracle_txs) = tokio::task::spawn_blocking(move || {
|
let (defi_txs, oracle_txs) = tokio::task::spawn_blocking(move || {
|
||||||
|
=======
|
||||||
|
let (cauldron_txs, oracle_txs, ido_txs) = tokio::task::spawn_blocking(move || {
|
||||||
|
>>>>>>> 7fb15f6 (ido: index unconfirmed txs from the mempool)
|
||||||
electrum_fetch_mempool(&electrum_clone.lock().unwrap())
|
electrum_fetch_mempool(&electrum_clone.lock().unwrap())
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
@ -182,6 +190,37 @@ pub async fn update_mempool(db: &DB, electrum: Arc<Mutex<Client>>) -> Result<()>
|
||||||
|
|
||||||
index_oracle(&db.oracle_w, &txs_to_add, &BlockHash::all_zeros()).await?;
|
index_oracle(&db.oracle_w, &txs_to_add, &BlockHash::all_zeros()).await?;
|
||||||
|
|
||||||
|
// ido updates
|
||||||
|
let ido_electrum = electrum.clone();
|
||||||
|
|
||||||
|
// filter txs already in an ido txchain
|
||||||
|
let mut ido_to_add = Vec::new();
|
||||||
|
for txid in ido_txs {
|
||||||
|
if !db::ido::has_txchain_tx(&db.ido_w, &txid).await? {
|
||||||
|
ido_to_add.push(txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let txs_to_add: Vec<Transaction> = tokio::task::spawn_blocking(move || {
|
||||||
|
ido_to_add
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(
|
||||||
|
|txid| match electrum_get_tx(&ido_electrum.lock().unwrap(), &txid) {
|
||||||
|
Ok(tx) => Some(tx),
|
||||||
|
Err(e) => {
|
||||||
|
info!("Failed to get mempool tx {txid}: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// an ido txchain 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).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,7 @@ async fn start_program(
|
||||||
|
|
||||||
// Avoid overlapping writer while indexer is on
|
// Avoid overlapping writer while indexer is on
|
||||||
if !indexing_in_progress_clone.load(Ordering::Relaxed) {
|
if !indexing_in_progress_clone.load(Ordering::Relaxed) {
|
||||||
if let Err(e) = update_mempool(&db, client.clone()).await {
|
if let Err(e) = update_mempool(&db, client.clone(), Some(network)).await {
|
||||||
error!("Failed to update mempool: {e}");
|
error!("Failed to update mempool: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue