// Copyright (C) 2024 Riften Labs AS // // 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::{ cmp, collections::{HashMap, HashSet, VecDeque}, convert::TryInto, }; use anyhow::Result; use bitcoin_hashes::{hex::ToHex, Hash}; use bitcoincash::BlockHash; use bitcoincash::{Script, TokenID, Transaction, Txid}; use log::debug; use riftenlabs_defi::chainutil::{compute_outpoint_hash, read_push_from_script, OutPointHash}; use rusqlite::Connection; use serde::{Deserialize, Serialize, Serializer}; use crate::db::bcmr::{get_matching_autheaders, insert_authheader, AuthChainEntry}; 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, } 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) } fn is_genesis_tx(tx: &Transaction) -> Option { let potential_token_ids: HashSet<[u8; 32]> = tx .input .iter() .filter(|i| i.previous_output.vout == 0) .map(|i| i.previous_output.txid.into_inner()) .collect(); for o in &tx.output { match &o.token { Some(token) => { if potential_token_ids.contains(token.id.as_inner()) { return Some(token.id); } } None => continue, } } None } pub fn ttor_sorted(txs: Vec) -> Vec { let txs = { let mut queue: VecDeque = txs.into_iter().collect(); let mut queue_txids: HashSet = queue.par_iter().map(|tx| tx.txid()).collect(); let mut txs: Vec = Vec::with_capacity(queue.len()); while let Some(tx) = queue.pop_front() { let mut has_parent = false; for i in &tx.input { if queue_txids.contains(&i.previous_output.txid) { // depends on parent has_parent = true; break; }; } if has_parent { queue.push_back(tx); } else { queue_txids.remove(&tx.txid()); txs.push(tx) } } txs }; txs } // 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, txs: Vec, ) -> Result { let sorted = ttor_sorted(txs); let mut new_tokens: Vec<(Transaction, TokenID)> = Vec::default(); let sorted: Vec = sorted .into_iter() .filter_map(|tx| { if let Some(token_id) = is_genesis_tx(&tx) { // filter out new; we know it to be start of authchain new_tokens.push((tx, token_id)); None } else { Some(tx) } }) .collect(); let mut candidates: HashMap = sorted .par_iter() .enumerate() .flat_map(|(tx_pos, tx)| { let tx_candidates: Vec<(OutPointHash, (&Transaction, usize))> = tx .input .iter() .filter_map(|i| { let prevout = &i.previous_output; if prevout.vout != 0 { // authchain needs to spend first output of a previous tx None } else { let utxohash = compute_outpoint_hash(&prevout.txid, prevout.vout); Some((utxohash, (tx, tx_pos))) } }) .collect(); tx_candidates }) .collect(); let mut inserts = 0; // Start by inserting new auth chains for (tx, token_id) in new_tokens { let bcmr = parse_bcmr(&tx); debug!( "Found token gensis for {} in {}; has bcmr: {}", token_id.to_hex(), tx.txid().to_hex(), bcmr.is_some() ); let txid = tx.txid(); let utxohash = compute_outpoint_hash(&tx.txid(), 0); insert_authheader( conn, &utxohash, blockhash, &txid, &token_id, 0, bcmr.map(|bcmr| bcmr.op_return), )?; inserts += 1; } // Finally; we need to loop for remaining updates; as each update has the potential to add more candidates. loop { let matches: Vec = get_matching_autheaders(conn, candidates.keys())?; if matches.is_empty() { break; } let mut smallest_index = usize::MAX; for prev_autheader in matches { let (tx, tx_pos) = candidates .remove(&prev_autheader.utxo) .expect("match not in candidate list"); let bcmr = parse_bcmr(tx); let txid = tx.txid(); let utxohash = compute_outpoint_hash(&txid, 0); debug!( "Found authheader update {} in {}; has bcmr: {}; new height: {}, utxo: {}", prev_autheader.token_id.to_hex(), txid.to_hex(), bcmr.is_some(), prev_autheader.height + 1, utxohash.to_hex() ); insert_authheader( conn, &utxohash, blockhash, &txid, &prev_autheader.token_id, prev_autheader.height + 1, bcmr.map(|bcmr| bcmr.op_return), )?; smallest_index = cmp::min(smallest_index, tx_pos); inserts += 1; } // Because transactions are TTOR sorted; there will be no new candidates above the "smallest index" autheader. candidates = candidates .into_par_iter() .filter(|(_, (_, tx_pos))| tx_pos > &smallest_index) .collect(); } Ok(inserts) } #[cfg(test)] mod tests { use bitcoincash::consensus::deserialize; use super::*; #[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] fn test_is_genesis_tx() { let tx_hex = "01000000032b70d6279de034fea76d4dd329c44a3e162b17a0cebc32eeeebbd14f5e8a97250100000064411b42a9dcb22c59ff0e800b3c63d6b8ac045bd18eb1effc8d2f23ec49ed0abdc316215a469a206d45153eac999d14eb8abc3d33b91c5ac4b61e7868d162d5d5184121021735db91ac477c1007da74c8c117c383eebc664d21f12871ca6e69401f8072f8feffffff2b70d6279de034fea76d4dd329c44a3e162b17a0cebc32eeeebbd14f5e8a972502000000644193e4d5bb533279fbcbebe15ba62f226e53e5c64a1624c755b255b13f0b1011e382182896026575370b5e30d03390d31faabf854a9f41c215b8a5bb57bae221df4121021735db91ac477c1007da74c8c117c383eebc664d21f12871ca6e69401f8072f8feffffff8d80b5567b6a3cac6e19a6934fd7544a02616fd6868a304c93bff6aed3eb12280100000064418b576b5defc8942182d01654cc2a0706b3e86e9031f7a13a694ffd830944ca00ecf5ad3271a242993580cbe78c13a41e35b06dfd68e72c4242dd7ff9e7e039a6412103b666cf8bc6ef2e3c3f100fc883013040f01f7085d75e5ab0811ae00b6a0a6d24feffffff03200300000000000040ef8d80b5567b6a3cac6e19a6934fd7544a02616fd6868a304c93bff6aed3eb1228600401db010076a914bfe9ed4ea9c7830c2ee96c17a3bd5ff563c12f2b88ac200300000000000040ef8d80b5567b6a3cac6e19a6934fd7544a02616fd6868a304c93bff6aed3eb1228600402db010076a914bfe9ed4ea9c7830c2ee96c17a3bd5ff563c12f2b88ac9f117901000000001976a91402a67e8d884367e7a22d6cda96a6df21e56a2f3288ac851b0c00"; let not_genesis_tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap(); let tx_hex = "0200000001925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb700000000644181707613f45069c8df981b2ef0cbd05158d5623602fac3ae2e13f69d05fa356c278f2f3a4e34f33a79e0f8375abd35051150ec024b3625807cea185528a5de9e412103b4680dffa1e34b3bfdb024fd0a8498eb24f59ba5ead34acfb74f9d8549db6f3a0000000003e8030000000000003eef925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb710fdf40176a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac0000000000000000456a0442434d5220c705cc90a56ac7ef9a15ef90ebbc8ba7e60e4c622e5464d52d8baf7887949fcc1d736f636b2e6361756c64726f6e2e71756573742f62636d722e6a736f6ed9210000000000001976a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac00000000"; let genesis_tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap(); assert!(!is_genesis_tx(¬_genesis_tx).is_some()); assert!(is_genesis_tx(&genesis_tx).is_some()); } }