// Copyright (C) 2024-2026 Whiterun LLC // // This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. // A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html use rayon::prelude::*; use std::convert::TryInto; use anyhow::Result; use bitcoin_hashes::{hex::FromHex, hex::ToHex, Hash}; use bitcoincash::{BlockHash, Script, TokenID, Transaction, Txid}; use log::debug; use riftenlabs_defi::chainutil::{compute_outpoint_hash, read_push_from_script, OutPointHash}; use rusqlite::{params, Connection}; use serde::{Deserialize, Serialize, Serializer}; use crate::{ db::bcmr::{insert_authheader, AuthChainEntry}, utiltoken::is_genesis_tx, }; pub mod bcmrdownloader; pub mod parsedbcmr; pub mod utilurl; pub mod wellknowndowloader; pub const BCMR_PREFIX: &[u8] = &[ 0x6a, // OP_RETURN 0x04, // PUSH 4 0x42, // B 0x43, // C 0x4d, // M 0x52, // R ]; // Custom function to serialize [u8; 32] as a hex string fn as_hex(bytes: &[u8; 32], serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&hex::encode(bytes)) } #[derive(Debug, Serialize, Deserialize)] #[allow(clippy::upper_case_acronyms)] pub struct BCMR { #[serde(serialize_with = "as_hex")] pub hash: [u8; 32], pub uris: Vec, #[serde(skip)] pub op_return: Vec, } fn find_parent_auth_entries(conn: &Connection, tx: &Transaction) -> Result> { let mut parents = Vec::new(); for vin in &tx.input { if vin.previous_output.vout != 0 { continue; } let prev = &vin.previous_output; let prev_utxo: OutPointHash = compute_outpoint_hash(&prev.txid, prev.vout); let mut stmt = conn.prepare( "SELECT token_id, txid, height, bcmr_data, utxo FROM auth_chain_entry WHERE utxo = ?1", )?; let mut rows = stmt.query(params![prev_utxo.to_hex()])?; while let Some(row) = rows.next()? { let token_hex: String = row.get(0)?; let txid_hex: String = row.get(1)?; let height: usize = row.get(2)?; let bcmr_data_hex: Option = row.get(3)?; let utxo_hex: String = row.get(4)?; let token_id = TokenID::from_hex(&token_hex)?; let txid = Txid::from_hex(&txid_hex)?; let utxo = OutPointHash::from_hex(&utxo_hex)?; let bcmr_data = match bcmr_data_hex { Some(h) => Some(hex::decode(&h)?), None => None, }; parents.push(AuthChainEntry { utxo, token_id, txid, height, bcmr_data, }); } } Ok(parents) } pub fn parse_bcmr_from_opreturn(bcmr_op_return: &Script) -> Option { let iter = bcmr_op_return[6..].iter(); let (iter, hash) = read_push_from_script(iter).ok()?; let hash: [u8; 32] = hash.and_then(|h| h.try_into().ok())?; let mut uris: Vec = vec![]; let mut uri_element; let mut uri_iter = iter; loop { (uri_iter, uri_element) = match read_push_from_script(uri_iter) { Ok(r) => r, Err(_) => break, }; match uri_element { Some(url) => { if let Ok(uri) = String::from_utf8(url) { uris.push(uri) } } None => break, } } Some(BCMR { hash, uris, op_return: bcmr_op_return.to_bytes(), }) } pub fn parse_bcmr(tx: &Transaction) -> Option { let bcmr_op_return = tx .output .iter() .find(|o| o.script_pubkey.as_bytes().starts_with(BCMR_PREFIX))?; parse_bcmr_from_opreturn(&bcmr_op_return.script_pubkey) } // TODO: Use when we get scriptpubkey filter for electrum.mempoo.get #[allow(dead_code)] pub fn mempool_index_genesis(conn: &Connection, txs: &Vec) -> Result { let genesis: Vec<(Transaction, TokenID)> = txs .par_iter() .filter_map(|tx| is_genesis_tx(tx).map(|token| (tx.clone(), token))) .collect(); let mut inserts = 0; for (tx, token) in genesis { let txid = tx.txid(); let utxo = compute_outpoint_hash(&txid, 0); let bcmr_data = parse_bcmr(&tx); insert_authheader( conn, &utxo, &BlockHash::all_zeros(), &txid, &token, 0, bcmr_data.map(|b| b.op_return), )?; inserts += 1; } Ok(inserts) } pub fn index_bcmr( conn: &Connection, blockhash: &BlockHash, sorted: Vec, // TTOR sorted! ) -> Result { let mut inserts = 0usize; for tx in sorted.iter() { // 1) Genesis path: start of auth chain (height 0) if let Some(token_id) = is_genesis_tx(tx) { let bcmr = parse_bcmr(tx); let txid = tx.txid(); let utxo = compute_outpoint_hash(&txid, 0); debug!( "Found token genesis for {} in {}; has bcmr: {}", token_id.to_hex(), txid.to_hex(), bcmr.is_some() ); insert_authheader( conn, &utxo, blockhash, &txid, &token_id, 0, bcmr.map(|b| b.op_return), )?; inserts += 1; continue; } let parents = find_parent_auth_entries(conn, tx)?; if parents.is_empty() { continue; } // 2) Update path: if any input spends a known auth utxo, extend that chain let bcmr = parse_bcmr(tx); let txid = tx.txid(); let new_utxo = compute_outpoint_hash(&txid, 0); for parent in parents { let new_height = parent.height + 1; debug!( "Found authheader update {} in {}; has bcmr: {}; new height: {}, utxo: {}", parent.token_id.to_hex(), txid.to_hex(), bcmr.is_some(), new_height, new_utxo.to_hex() ); insert_authheader( conn, &new_utxo, blockhash, &txid, &parent.token_id, new_height, bcmr.as_ref().map(|b| b.op_return.clone()), )?; inserts += 1; } } Ok(inserts) } #[test] fn multi_parent_auth_update_creates_entries_for_all_tokens() { use crate::db::bcmr::prepare_tables; use bitcoincash::{OutPoint, PackedLockTime, Sequence, TxIn, TxOut}; // In-memory DB let mut conn = Connection::open_in_memory().unwrap(); prepare_tables(&conn); let db_tx = conn.transaction().unwrap(); // <-- DB transaction let bh = BlockHash::all_zeros(); // Two fake token IDs let token_a = TokenID::from_hex("0101010101010101010101010101010101010101010101010101010101010101") .unwrap(); let token_b = TokenID::from_hex("0202020202020202020202020202020202020202020202020202020202020202") .unwrap(); // Two fake previous txids (current heads for A and B) let prev_txid_a = Txid::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); let prev_txid_b = Txid::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(); // Their auth-head UTXOs (vout 0) let utxo_a = compute_outpoint_hash(&prev_txid_a, 0); let utxo_b = compute_outpoint_hash(&prev_txid_b, 0); // Insert both auth heads at height 0 insert_authheader(&db_tx, &utxo_a, &bh, &prev_txid_a, &token_a, 0, None).unwrap(); insert_authheader(&db_tx, &utxo_b, &bh, &prev_txid_b, &token_b, 0, None).unwrap(); // Build a single tx that spends BOTH auth heads let input_a = TxIn { previous_output: OutPoint { txid: prev_txid_a, vout: 0, }, script_sig: Script::new(), sequence: Sequence(0xFFFF_FFFF), ..Default::default() }; let input_b = TxIn { previous_output: OutPoint { txid: prev_txid_b, vout: 0, }, script_sig: Script::new(), sequence: Sequence(0xFFFF_FFFF), ..Default::default() }; let output0 = TxOut { value: 0, script_pubkey: Script::new(), ..Default::default() }; let update_tx = Transaction { version: 2, lock_time: PackedLockTime(0), input: vec![input_a, input_b], output: vec![output0], }; let update_txid = update_tx.txid(); // Index this one tx as if it's in a block index_bcmr(&db_tx, &bh, vec![update_tx]).unwrap(); // ---- ASSERTIONS ---- // Expect 2 auth_chain_entry rows for this tx (one per token) { let mut stmt = db_tx .prepare("SELECT COUNT(*) FROM auth_chain_entry WHERE txid = ?") .unwrap(); let count: i64 = stmt .query_row([update_txid.to_hex()], |row| row.get(0)) .unwrap(); assert_eq!( count, 2, "Expected 2 auth_chain_entry rows for multi-parent auth update tx" ); } // Expect each token to have height 1 in this tx { let mut stmt = db_tx .prepare( "SELECT token_id, height FROM auth_chain_entry WHERE txid = ? ORDER BY token_id", ) .unwrap(); let mut rows = stmt.query([update_txid.to_hex()]).unwrap(); let mut seen: Vec<(String, i64)> = Vec::new(); while let Some(row) = rows.next().unwrap() { let t: String = row.get(0).unwrap(); let h: i64 = row.get(1).unwrap(); seen.push((t, h)); } assert_eq!(seen.len(), 2, "expected 2 rows for update tx"); seen.sort_by(|a, b| a.0.cmp(&b.0)); assert_eq!(seen[0].0, token_a.to_hex()); assert_eq!(seen[0].1, 1); assert_eq!(seen[1].0, token_b.to_hex()); assert_eq!(seen[1].1, 1); } // No need to commit in the test; we're only asserting state // db_tx.commit().unwrap(); } #[cfg(test)] mod tests { use bitcoincash::{consensus::deserialize, Block}; use crate::utiltx::ttor_sorted_kahn; use super::*; use electrum_client_netagnostic::{Client, ElectrumApi, Param}; use std::collections::HashMap; #[test] fn test_parse_bcmr() { let tx_hex = "0200000001925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb700000000644181707613f45069c8df981b2ef0cbd05158d5623602fac3ae2e13f69d05fa356c278f2f3a4e34f33a79e0f8375abd35051150ec024b3625807cea185528a5de9e412103b4680dffa1e34b3bfdb024fd0a8498eb24f59ba5ead34acfb74f9d8549db6f3a0000000003e8030000000000003eef925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb710fdf40176a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac0000000000000000456a0442434d5220c705cc90a56ac7ef9a15ef90ebbc8ba7e60e4c622e5464d52d8baf7887949fcc1d736f636b2e6361756c64726f6e2e71756573742f62636d722e6a736f6ed9210000000000001976a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac00000000"; let tx: Transaction = deserialize(&hex::decode(tx_hex).unwrap()).unwrap(); let result = parse_bcmr(&tx); assert!(result.is_some()); let bcmr = result.unwrap(); assert_eq!( hex::encode(bcmr.hash), "c705cc90a56ac7ef9a15ef90ebbc8ba7e60e4c622e5464d52d8baf7887949fcc" ); assert_eq!(bcmr.uris.len(), 1); assert_eq!(bcmr.uris[0], "sock.cauldron.quest/bcmr.json"); } #[test] fn invalid_bcmr_hash() { // txid 4e44c45cf1fb956fe4752c6dfa9637f29cf18e9251e20454dd65dbcc298617b6 let tx_hex = "0200000002020056e81cda3b55f6b912acf05131e4f50594dc76702e8022d5522582f74bd40000000064414048069ab85d72e0f195e6587a4ef30360d5304a7d24995e21f8bb3d9c9c52eb5d1408a3a4754fad8da6593dc65fd248e507fe1620004bcb87f3237fcea9679e4121028bb6fdf7137233695237a6fa538827afb4f7490bad82215098b40dc3d9c645fc00000000020056e81cda3b55f6b912acf05131e4f50594dc76702e8022d5522582f74bd4010000006441d501d321b457b2622388efc9290245e7021f082d2914bf1883184ea29f546a2b08d74537f848fba17d41e44ad6f9d6949b44670b379635b00fe69caa5236e47f4121028bb6fdf7137233695237a6fa538827afb4f7490bad82215098b40dc3d9c645fc0000000003e80300000000000042ef020056e81cda3b55f6b912acf05131e4f50594dc76702e8022d5522582f74bd431ff00b402869d7e0100a9147ca730ccf0ba8c552bae1287d01a9e7e5590f0fa870000000000000000d56a0442434d5240646331303939346363323530363731643830383737626639356131613136616164356431343033623365623263373136346366623231383532323363353162664c506e667473746f726167652e6c696e6b2f697066732f6261666b726569673463636d757a7173716d346f796262333337666e627566766b32786975616f7a36776c64726d746833656763736570637278343b6261666b726569673463636d757a7173716d346f796262333337666e627566766b32786975616f7a36776c64726d74683365676373657063727834bb0f0000000000001976a914d948abf743a76f472703b6a91437854dc1d7ced588ac00000000"; let tx: Transaction = deserialize(&hex::decode(tx_hex).unwrap()).unwrap(); let result = parse_bcmr(&tx); // The hash value in this BCMR is 64 bytes. Not a valid sha256 hash. assert!(result.is_none()); } #[test] #[ignore] // Integration test - requires network access. Run with `cargo test moria_issue --ignored` fn moria_issue() { // Test that the indexer is able to follow the auth chain for MORIA token (part of issue #15). // Map: block height -> expected txid of BCMR auth update transaction let blocks_with_update: HashMap = [ ( 897958, "06ef2e1f62b1efbf8a59476ae4f6d642a7eaf85a19d015909639211a7dec6a31", ), ( 897958, "d9cc6381823202f7c6df06c90be9748e51912f57a5d1475ac3ad5f627093e507", ), ( 897957, "6b76cfdadd3b70aa5439d1f2d4cbfc49d52b59ca45e3a3c353be7318daa105a2", ), ( 897957, "17f6c81f4c6a95a14e542de8d9c77b292a16ee1d03c40f2fdcb9ab5043ee318b", ), // genesis ] .iter() .cloned() .collect(); let client = Client::new(&format!("tcp://rostrum.cauldron.quest:50001")).unwrap(); let mut conn = Connection::open_in_memory().unwrap(); crate::db::bcmr::prepare_tables(&conn); let tx = conn.transaction().unwrap(); // Process blocks in ascending order (lowest to highest) let mut sorted_blocks: Vec<_> = blocks_with_update.iter().collect(); sorted_blocks.sort_by(|a, b| a.0.cmp(b.0)); for (block_height, expected_hash) in sorted_blocks { let block = client .raw_call("blockchain.block.get", vec![Param::U32(*block_height)]) .unwrap(); let block: Block = deserialize(&hex::decode(block.as_str().unwrap()).unwrap()).unwrap(); // Process this block immediately let block_hash = block.block_hash(); let sorted_txs = ttor_sorted_kahn(block.txdata); let expected_txid = *expected_hash; // Index the block index_bcmr(&tx, &block_hash, sorted_txs).unwrap(); let mut stmt = tx .prepare("SELECT COUNT(*) FROM auth_chain_entry WHERE txid = ?") .unwrap(); let count: i64 = stmt .query_row([expected_txid], |row| row.get(0)) .unwrap_or(0); assert_eq!( count, 1, "Expected 1 BCMR auth update for txid: {} in block {}", expected_txid, block_height ); } tx.commit().unwrap(); } #[test] #[ignore] // Integration test - requires network access. Run with `cargo test furu_issue --ignored` fn furu_issue() { // Test that the indexer is able to follow the auth chain for FURU token (part of issue #15). // Map: block height -> expected txid of BCMR auth update transaction let blocks_with_update: HashMap = [ ( 857450, "c2cb85935d09e34f12b8b61e93efffc49c4f73a243748e53317ce6cd208de3dc", ), ( 857273, "46cf4038b461ebbc8fb4268e74c8ec18e0beac0290ed91b7c2d2a0ddfe723c08", ), ( 819537, "7c54400ebe360902acaaf7696fe0c0eee4f6dc5843425fbf335154bbf2445f5a", ), ( 817442, "b5067f71ae01dd382c126c18f07f602586911f81cfaae442dfac228886af1b66", ), ( 817438, "5e3915d6ab19c7389a531c7e8144e6d286f48bbc2db48588c27975047ff400aa", ), // genesis ] .iter() .cloned() .collect(); let client = Client::new(&format!("tcp://rostrum.cauldron.quest:50001")).unwrap(); let mut conn = Connection::open_in_memory().unwrap(); crate::db::bcmr::prepare_tables(&conn); let tx = conn.transaction().unwrap(); // Process blocks in ascending order (lowest to highest) let mut sorted_blocks: Vec<_> = blocks_with_update.iter().collect(); sorted_blocks.sort_by(|a, b| a.0.cmp(b.0)); for (block_height, expected_hash) in sorted_blocks { let block = client .raw_call("blockchain.block.get", vec![Param::U32(*block_height)]) .unwrap(); let block: Block = deserialize(&hex::decode(block.as_str().unwrap()).unwrap()).unwrap(); // Process this block immediately let block_hash = block.block_hash(); let sorted_txs = ttor_sorted_kahn(block.txdata); let expected_txid = *expected_hash; // Index the block index_bcmr(&tx, &block_hash, sorted_txs).unwrap(); let mut stmt = tx .prepare("SELECT COUNT(*) FROM auth_chain_entry WHERE txid = ?") .unwrap(); let count: i64 = stmt .query_row([expected_txid], |row| row.get(0)) .unwrap_or(0); assert_eq!( count, 1, "Expected 1 BCMR auth update for txid: {} in block {}", expected_txid, block_height ); } tx.commit().unwrap(); } #[test] #[ignore] fn olando_issue() { // Integration test - requires network access. Run with `cargo test olando_issue --ignored` // Test that the indexer is able to follow the auth chain for OLANDO token (part of issue #15). // Map: block height -> expected txid of BCMR auth update transaction let blocks_with_update: HashMap = [ ( 919723, "1987b8c8114c2cf8492467da319a8856e81f49345f6385e6992cd820871bef87", ), ( 917546, "6c422e57f64a2c21d3e9da9161e483393da17ee1a0d8aea3ebf9e6c944ddf473", ), ( 911009, "de926cb4c086ce4c0cd79379a53149c54a21f43ccab322bef8429d60cd53a6a1", ), ( 910728, "536e199357a02fe9ea071cf7d0df4431911fd2ad170867659e796f053f7e6045", ), ( 908242, "0370d0ae6f1f0c7b49b5c3f7e271a1ebf4daef9d91450f2316e878a0a78d6d08", ), ( 906927, "68a0067153db9925560d7adb956295b41de958058ccd78d5c1adfdf4e0e70ed9", ), ( 906218, "20e8074d404e5e80c6d8cbe30d51c754ab86ae0b58bc8ec17f12afdb6380195d", ), ( 887082, "6a806f2e01e11a2a4000bb736ffb5570dd55412582088abe95fce700a17a3922", ), ( 887074, "7c71bbe3e3f52c5bf9965f454e7f87b86f097ecfec0705360e9284f38c360d65", ), // genesis ] .iter() .cloned() .collect(); let client = Client::new(&format!("tcp://rostrum.cauldron.quest:50001")).unwrap(); let mut conn = Connection::open_in_memory().unwrap(); crate::db::bcmr::prepare_tables(&conn); let tx = conn.transaction().unwrap(); // Process blocks in ascending order (lowest to highest) let mut sorted_blocks: Vec<_> = blocks_with_update.iter().collect(); sorted_blocks.sort_by(|a, b| a.0.cmp(b.0)); for (block_height, expected_hash) in sorted_blocks { let block = client .raw_call("blockchain.block.get", vec![Param::U32(*block_height)]) .unwrap(); let block: Block = deserialize(&hex::decode(block.as_str().unwrap()).unwrap()).unwrap(); // Process this block immediately let block_hash = block.block_hash(); let sorted_txs = ttor_sorted_kahn(block.txdata); let expected_txid = *expected_hash; // Index the block index_bcmr(&tx, &block_hash, sorted_txs).unwrap(); let mut stmt = tx .prepare("SELECT COUNT(*) FROM auth_chain_entry WHERE txid = ?") .unwrap(); let count: i64 = stmt .query_row([expected_txid], |row| row.get(0)) .unwrap_or(0); assert_eq!( count, 1, "Expected 1 BCMR auth update for txid: {} in block {}", expected_txid, block_height ); } tx.commit().unwrap(); } }