Files
brk/crates/brk_computer/src/transactions/patterns/compute.rs
T
2026-07-16 17:39:05 +02:00

252 lines
9.2 KiB
Rust

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));
}
}