use crate::Transaction;
use sha2::{Digest, Sha256};
use std::collections::HashSet;
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: 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> {
if self.bit_count > 0 {
self.data.push(self.current_byte);
}
self.data
}
}
fn golomb_rice_encode(value: u64, p: u8) -> Vec<u8> {
let mut writer = BitWriter::new();
let quotient = value >> p; let remainder = value & ((1u64 << p) - 1);
for _ in 0..quotient {
writer.write_bit(true);
}
writer.write_bit(false);
writer.write_bits(remainder, p);
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)
}
#[allow(dead_code)]
fn bit_offset(&self) -> usize {
self.bit_offset
}
}
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 mut filter_set = HashSet::new();
for tx in block_transactions {
for output in &tx.outputs {
if !output.script_pubkey.is_empty() {
filter_set.insert(output.script_pubkey.clone());
}
}
}
for script in previous_outpoint_scripts {
if !script.is_empty() {
filter_set.insert(script.clone());
}
}
let n = filter_set.len() as u64;
if n == 0 {
return Ok(CompactBlockFilter {
filter_data: Vec::new(),
num_elements: 0,
});
}
let mut hashed_values: Vec<u64> = filter_set
.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 mut differences = Vec::new();
if !hashed_values.is_empty() {
differences.push(hashed_values[0]);
for i in 1..hashed_values.len() {
let diff = hashed_values[i] - hashed_values[i - 1];
differences.push(diff);
}
}
let mut filter_data = Vec::new();
for diff in differences {
let encoded = golomb_rice_encode(diff, BIP158_P);
filter_data.extend_from_slice(&encoded);
}
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);
let mut decoded_values = Vec::new();
let mut current_value = 0u64;
for _ in 0..filter.num_elements {
if let Some(diff) = golomb_rice_decode(&mut reader, BIP158_P) {
current_value += diff;
decoded_values.push(current_value);
} else {
return false;
}
}
decoded_values.binary_search(&script_hash).is_ok()
}
#[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_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);
}
}