use crate::Transaction;
use sha2::{Digest, Sha256};
use std::sync::Arc;
pub const BIP158_P: u8 = 19;
pub const BIP158_M: u64 = 1 << BIP158_P;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompactBlockFilter {
pub filter_data: Arc<Vec<u8>>,
pub num_elements: u32,
}
fn hash_to_range(script: &[u8], n: u64, m: u64) -> u64 {
let mut hasher = Sha256::new();
hasher.update(script);
let hash = hasher.finalize();
let hash_value = u64::from_le_bytes([
hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7],
]);
hash_value % (n * m)
}
struct BitWriter {
data: Vec<u8>,
current_byte: u8,
bit_count: u8,
}
impl BitWriter {
fn new() -> Self {
Self {
data: Vec::new(),
current_byte: 0,
bit_count: 0,
}
}
fn write_bit(&mut self, bit: bool) {
if bit {
self.current_byte |= 1u8 << (7 - self.bit_count);
}
self.bit_count += 1;
if self.bit_count == 8 {
self.data.push(self.current_byte);
self.current_byte = 0;
self.bit_count = 0;
}
}
fn write_bits(&mut self, value: u64, num_bits: u8) {
for i in 0..num_bits {
let bit = ((value >> (num_bits - 1 - i)) & 1) != 0;
self.write_bit(bit);
}
}
fn finish(mut self) -> Vec<u8> {
self.align_byte();
self.data
}
fn align_byte(&mut self) {
if self.bit_count > 0 {
self.data.push(self.current_byte);
self.current_byte = 0;
self.bit_count = 0;
}
}
fn write_golomb_rice(&mut self, value: u64, p: u8) {
let quotient = value >> p;
let remainder = value & ((1u64 << p) - 1);
for _ in 0..quotient {
self.write_bit(true);
}
self.write_bit(false);
self.write_bits(remainder, p);
}
}
#[cfg(test)]
fn golomb_rice_encode(value: u64, p: u8) -> Vec<u8> {
let mut writer = BitWriter::new();
writer.write_golomb_rice(value, p);
writer.finish()
}
fn encode_sorted_differences(values: &[u64]) -> Vec<u8> {
let mut writer = BitWriter::new();
let Some((first, rest)) = values.split_first() else {
return writer.finish();
};
writer.write_golomb_rice(*first, BIP158_P);
writer.align_byte();
let mut prev = *first;
for value in rest {
writer.write_golomb_rice(value - prev, BIP158_P);
writer.align_byte();
prev = *value;
}
writer.finish()
}
struct BitReader<'a> {
data: &'a [u8],
bit_offset: usize,
}
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self {
data,
bit_offset: 0,
}
}
fn read_bit(&mut self) -> Option<bool> {
if self.bit_offset >= self.data.len() * 8 {
return None;
}
let byte_idx = self.bit_offset / 8;
let bit_idx = self.bit_offset % 8;
let bit = (self.data[byte_idx] >> (7 - bit_idx)) & 1;
self.bit_offset += 1;
Some(bit == 1)
}
fn read_bits(&mut self, p: u8) -> Option<u64> {
let mut value = 0u64;
for _ in 0..p {
if let Some(bit) = self.read_bit() {
value = (value << 1) | (if bit { 1 } else { 0 });
} else {
return None;
}
}
Some(value)
}
}
fn golomb_rice_decode(reader: &mut BitReader, p: u8) -> Option<u64> {
let mut quotient = 0u64;
loop {
match reader.read_bit() {
Some(true) => quotient += 1,
Some(false) => break,
None => return None,
}
}
let remainder = reader.read_bits(p)?;
Some((quotient << p) | remainder)
}
pub fn build_block_filter(
block_transactions: &[Transaction],
previous_outpoint_scripts: &[Vec<u8>],
) -> Result<CompactBlockFilter, String> {
let refs: Vec<&[u8]> = previous_outpoint_scripts
.iter()
.map(Vec::as_slice)
.collect();
build_block_filter_slices(block_transactions, &refs)
}
pub fn build_block_filter_slices<'a>(
block_transactions: &'a [Transaction],
previous_outpoint_scripts: &[&'a [u8]],
) -> Result<CompactBlockFilter, String> {
let mut output_scripts = Vec::new();
for tx in block_transactions {
for output in &tx.outputs {
output_scripts.push(output.script_pubkey.as_slice());
}
}
build_block_filter_output_scripts(output_scripts, previous_outpoint_scripts)
}
pub fn build_block_filter_output_scripts<'a>(
mut scripts: Vec<&'a [u8]>,
previous_outpoint_scripts: &[&'a [u8]],
) -> Result<CompactBlockFilter, String> {
scripts.retain(|script| !script.is_empty());
scripts.extend(
previous_outpoint_scripts
.iter()
.copied()
.filter(|script| !script.is_empty()),
);
scripts.sort_unstable();
scripts.dedup();
let n = scripts.len() as u64;
if n == 0 {
return Ok(CompactBlockFilter {
filter_data: Arc::new(Vec::new()),
num_elements: 0,
});
}
let mut hashed_values: Vec<u64> = scripts
.iter()
.map(|script| hash_to_range(script, n, BIP158_M))
.collect();
hashed_values.sort_unstable();
hashed_values.dedup();
let n_final = hashed_values.len() as u64;
let filter_data = Arc::new(encode_sorted_differences(&hashed_values));
Ok(CompactBlockFilter {
filter_data,
num_elements: n_final as u32,
})
}
pub fn match_filter(filter: &CompactBlockFilter, script: &[u8]) -> bool {
if filter.num_elements == 0 {
return false;
}
let n = filter.num_elements as u64;
let script_hash = hash_to_range(script, n, BIP158_M);
let mut reader = BitReader::new(filter.filter_data.as_slice());
let mut current_value = 0u64;
let mut found = false;
for _ in 0..filter.num_elements {
let Some(diff) = golomb_rice_decode(&mut reader, BIP158_P) else {
return false;
};
current_value += diff;
if current_value == script_hash {
found = true;
}
}
found
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_to_range() {
let script = b"test script";
let n = 100;
let m = BIP158_M;
let value = hash_to_range(script, n, m);
assert!(value < n * m);
}
#[test]
fn test_golomb_rice_encode() {
let encoded = golomb_rice_encode(0, BIP158_P);
assert!(!encoded.is_empty());
let encoded2 = golomb_rice_encode(1, BIP158_P);
assert!(!encoded2.is_empty());
}
#[test]
fn test_empty_filter() {
let filter = build_block_filter(&[], &[]).unwrap();
assert_eq!(filter.num_elements, 0);
assert!(filter.filter_data.is_empty());
}
#[test]
fn test_joined_golomb_matches_separate_encodings() {
let values = [0u64, 1, 100, 10_000];
let mut separate = Vec::new();
for value in values {
separate.extend(golomb_rice_encode(value, BIP158_P));
}
let mut writer = BitWriter::new();
for value in values {
writer.write_golomb_rice(value, BIP158_P);
writer.align_byte();
}
assert_eq!(writer.finish(), separate);
}
#[test]
fn test_golomb_rice_encode_decode_roundtrip() {
let test_values = vec![0, 1, 2, 10, 100, 1000, 10000];
for value in test_values {
let encoded = golomb_rice_encode(value, BIP158_P);
let mut reader = BitReader::new(&encoded);
let decoded = golomb_rice_decode(&mut reader, BIP158_P);
assert_eq!(decoded, Some(value), "Roundtrip failed for value {value}");
}
}
#[test]
fn test_build_and_match_filter() {
use crate::{OutPoint, Transaction, TransactionInput, TransactionOutput};
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0u8; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![blvm_consensus::opcodes::OP_1, blvm_consensus::opcodes::OP_2],
}]
.into(),
lock_time: 0,
};
let filter = build_block_filter(&[tx.clone()], &[]).unwrap();
assert!(filter.num_elements > 0);
let script_in_filter = &tx.outputs[0].script_pubkey;
assert!(match_filter(&filter, script_in_filter));
let script_not_in_filter = vec![0x53, 0x54]; let _matched = match_filter(&filter, &script_not_in_filter);
let truncated = CompactBlockFilter {
filter_data: filter.filter_data.clone(),
num_elements: filter.num_elements + 1,
};
assert!(!match_filter(&truncated, script_in_filter));
}
#[test]
fn test_filter_bytes_ignore_script_order() {
use crate::{Transaction, TransactionInput, TransactionOutput};
let tx = |scripts: &[&[u8]]| Transaction {
version: 1,
inputs: Vec::<TransactionInput>::new().into(),
outputs: scripts
.iter()
.map(|script| TransactionOutput {
value: 1,
script_pubkey: script.to_vec(),
})
.collect::<Vec<_>>()
.into(),
lock_time: 0,
};
let first = build_block_filter(&[tx(&[b"\x51\x52", b"\x53\x54"])], &[]).unwrap();
let second = build_block_filter(&[tx(&[b"\x53\x54", b"\x51\x52"])], &[]).unwrap();
assert_eq!(first, second);
assert_eq!(first.num_elements, 2);
}
}