use brk_error::Result; use brk_indexer::Indexer; use brk_types::{StoredBool, StoredU64, TxIndex}; use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec}; use super::{Vecs, coinjoin::Candidate}; use crate::{indexes, inputs}; const WRITE_INTERVAL: usize = 10_000; impl Vecs { pub(crate) fn compute( &mut self, indexer: &Indexer, indexes: &indexes::Vecs, spent: &inputs::SpentVecs, exit: &Exit, ) -> Result<()> { let features = &indexer.vecs.transaction_features; let version = indexes.tx_index.input_count.version() + indexes.tx_index.output_count.version() + indexer.vecs.transactions.first_tx_index.version() + indexer.vecs.transactions.first_txin_index.version() + indexer.vecs.transactions.first_txout_index.version() + spent.value.version() + indexer.vecs.inputs.output_type.version() + indexer.vecs.inputs.type_index.version() + indexer.vecs.outputs.value.version() + indexer.vecs.outputs.output_type.version() + indexer.vecs.outputs.type_index.version() + features.has_op_return.version() + features.has_inscription.version() + indexes.height.tx_index_count.version(); for vec in [ &mut self.is_coinjoin, &mut self.is_consolidation, &mut self.is_batch_payout, ] { vec.validate_computed_version_or_reset(version)?; } for vec in [ &mut self.count.coinjoin, &mut self.count.consolidation, &mut self.count.batch_payout, ] { vec.validate_computed_version_or_reset(version)?; } let lengths = indexer.safe_lengths(); let target_tx = lengths.tx_index.to_usize(); let target_height = lengths.height.to_usize(); let tx_len = self .is_coinjoin .len() .min(self.is_consolidation.len()) .min(self.is_batch_payout.len()) .min(target_tx); let count_len = self .count .coinjoin .len() .min(self.count.consolidation.len()) .min(self.count.batch_payout.len()) .min(target_height); let start_height = count_len.min(next_height(indexes, tx_len, target_tx, target_height)); if start_height >= target_height { return Ok(()); } let first_tx = &indexer.vecs.transactions.first_tx_index; let start_tx = first_tx.collect_one_at(start_height).unwrap().to_usize(); self.is_coinjoin.truncate_if_needed_at(start_tx)?; self.is_consolidation.truncate_if_needed_at(start_tx)?; self.is_batch_payout.truncate_if_needed_at(start_tx)?; self.count.coinjoin.truncate_if_needed_at(start_height)?; self.count .consolidation .truncate_if_needed_at(start_height)?; self.count .batch_payout .truncate_if_needed_at(start_height)?; let first_txin = indexer .vecs .transactions .first_txin_index .collect_one_at(start_tx) .unwrap() .to_usize(); let first_txout = indexer .vecs .transactions .first_txout_index .collect_one_at(start_tx) .unwrap() .to_usize(); let mut input_count = indexes.tx_index.input_count.cursor(); let mut output_count = indexes.tx_index.output_count.cursor(); let mut input_value = spent.value.cursor(); let mut input_type = indexer.vecs.inputs.output_type.cursor(); let mut input_type_index = indexer.vecs.inputs.type_index.cursor(); let mut output_value = indexer.vecs.outputs.value.cursor(); let mut output_type = indexer.vecs.outputs.output_type.cursor(); let mut output_type_index = indexer.vecs.outputs.type_index.cursor(); let mut has_op_return = features.has_op_return.cursor(); let mut has_inscription = features.has_inscription.cursor(); let mut tx_count = indexes.height.tx_index_count.cursor(); input_count.advance(start_tx); output_count.advance(start_tx); input_value.advance(first_txin); input_type.advance(first_txin); input_type_index.advance(first_txin); output_value.advance(first_txout); output_type.advance(first_txout); output_type_index.advance(first_txout); has_op_return.advance(start_tx); has_inscription.advance(start_tx); tx_count.advance(start_height); let mut candidate = Candidate::default(); let mut block_start = start_tx; for height in start_height..target_height { let block_end = block_start + u64::from(tx_count.next().unwrap()) as usize; let mut coinjoin_count = 0; let mut consolidation_count = 0; let mut batch_payout_count = 0; for tx_index in block_start..block_end { let inputs = usize::from(input_count.next().unwrap()); let outputs = usize::from(output_count.next().unwrap()); let op_return = has_op_return.next().unwrap().is_true(); let inscription = has_inscription.next().unwrap().is_true(); let token_related = op_return || inscription; let consolidation = is_consolidation(inputs, outputs); let batch_payout = is_batch_payout(inputs, outputs, tx_index == block_start); let coinjoin_candidate = is_coinjoin_candidate(inputs, outputs, token_related); let coinjoin = if coinjoin_candidate { candidate.clear(); for _ in 0..inputs { candidate.add_input( input_value.next().unwrap(), input_type.next().unwrap(), input_type_index.next().unwrap(), ); } for _ in 0..outputs { candidate.add_output( output_value.next().unwrap(), output_type.next().unwrap(), output_type_index.next().unwrap(), ); } candidate.is_match(inputs, outputs) } else { input_value.advance(inputs); input_type.advance(inputs); input_type_index.advance(inputs); output_value.advance(outputs); output_type.advance(outputs); output_type_index.advance(outputs); false }; coinjoin_count += coinjoin as u64; consolidation_count += consolidation as u64; batch_payout_count += batch_payout as u64; self.is_coinjoin.push(StoredBool::from(coinjoin)); self.is_consolidation.push(StoredBool::from(consolidation)); self.is_batch_payout.push(StoredBool::from(batch_payout)); } self.count.coinjoin.push(StoredU64::from(coinjoin_count)); self.count .consolidation .push(StoredU64::from(consolidation_count)); self.count .batch_payout .push(StoredU64::from(batch_payout_count)); if (height + 1).is_multiple_of(WRITE_INTERVAL) { let _lock = exit.lock(); self.write()?; } block_start = block_end; } let _lock = exit.lock(); self.write() } fn write(&mut self) -> Result<()> { self.is_coinjoin.write()?; self.is_consolidation.write()?; self.is_batch_payout.write()?; self.count.coinjoin.write()?; self.count.consolidation.write()?; self.count.batch_payout.write()?; Ok(()) } } fn next_height( indexes: &indexes::Vecs, tx_len: usize, target_tx: usize, target_height: usize, ) -> usize { if tx_len >= target_tx { target_height } else { indexes .tx_heights .get_shared(TxIndex::from(tx_len)) .unwrap() .to_usize() } } fn is_consolidation(inputs: usize, outputs: usize) -> bool { inputs >= outputs * 5 } fn is_batch_payout(inputs: usize, outputs: usize, is_coinbase: bool) -> bool { !is_coinbase && outputs >= inputs * 5 } fn is_coinjoin_candidate(inputs: usize, outputs: usize, token_related: bool) -> bool { inputs >= 5 && outputs >= 5 && inputs < outputs * 5 && outputs < inputs * 5 && !token_related } #[cfg(test)] mod tests { use super::{is_batch_payout, is_coinjoin_candidate, is_consolidation}; #[test] fn ratio_boundaries_match_filter_semantics() { assert!(is_consolidation(25, 5)); assert!(is_batch_payout(5, 25, false)); assert!(!is_batch_payout(1, 5, true)); assert!(!is_coinjoin_candidate(25, 5, false)); assert!(!is_coinjoin_candidate(5, 25, false)); assert!(is_coinjoin_candidate(5, 5, false)); assert!(!is_coinjoin_candidate(5, 5, true)); } }