riftenlabs-indexer/src/bcmr/mod.rs

339 lines
13 KiB
Rust
Raw Normal View History

// 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 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<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
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<String>,
#[serde(skip)]
pub op_return: Vec<u8>,
}
pub fn parse_bcmr_from_opreturn(bcmr_op_return: &Script) -> Option<BCMR> {
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<String> = 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<BCMR> {
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<TokenID> {
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<Transaction>) -> Vec<Transaction> {
let txs = {
let mut queue: VecDeque<Transaction> = txs.into_iter().collect();
let mut queue_txids: HashSet<Txid> = queue.par_iter().map(|tx| tx.txid()).collect();
let mut txs: Vec<Transaction> = 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<Transaction>) -> Result<usize> {
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<Transaction>,
) -> Result<usize> {
let sorted = ttor_sorted(txs);
let mut new_tokens: Vec<(Transaction, TokenID)> = Vec::default();
let sorted: Vec<Transaction> = 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<OutPointHash, (&Transaction, usize)> = 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<AuthChainEntry> = 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(&not_genesis_tx).is_some());
assert!(is_genesis_tx(&genesis_tx).is_some());
}
}