Skip to main content

blvm_protocol/
bip158.rs

1//! BIP158: Compact Block Filters for Light Client Discovery
2//!
3//! Specification: https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki
4//!
5//! Implements Golomb-Rice Coded Sets (GCS) for efficient block filtering.
6//! Allows light clients to determine if a block contains relevant transactions
7//! without downloading the entire block.
8
9use crate::Transaction;
10use crate::bip152::siphash24;
11use std::sync::Arc;
12
13/// Golomb-Rice parameter P. The false-positive rate is about 1 in 2^P.
14pub const BIP158_P: u8 = 19;
15
16/// Range multiplier. F = N * BIP158_M.
17pub const BIP158_M: u64 = 784_931;
18
19/// Compact block filter (GCS)
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct CompactBlockFilter {
22    /// Golomb-Rice encoded filter data. Shared with a cached `cfilter` message.
23    pub filter_data: Arc<Vec<u8>>,
24    /// Number of elements in the filter
25    pub num_elements: u32,
26}
27
28fn filter_key(block_hash: &[u8; 32]) -> (u64, u64) {
29    let k0 = u64::from_le_bytes(block_hash[0..8].try_into().expect("8"));
30    let k1 = u64::from_le_bytes(block_hash[8..16].try_into().expect("8"));
31    (k0, k1)
32}
33
34/// `(siphash24(k0, k1, script) as u128 * F as u128) >> 64`.
35fn map_script(k0: u64, k1: u64, script: &[u8], f: u128) -> u64 {
36    let hashed = siphash24(k0, k1, script) as u128;
37    ((hashed.saturating_mul(f)) >> 64) as u64
38}
39
40/// Bit writer for Golomb-Rice encoding
41struct BitWriter {
42    data: Vec<u8>,
43    current_byte: u8,
44    bit_count: u8,
45}
46
47impl BitWriter {
48    fn new() -> Self {
49        Self {
50            data: Vec::new(),
51            current_byte: 0,
52            bit_count: 0,
53        }
54    }
55
56    /// Write a single bit
57    fn write_bit(&mut self, bit: bool) {
58        if bit {
59            self.current_byte |= 1u8 << (7 - self.bit_count);
60        }
61        self.bit_count += 1;
62
63        if self.bit_count == 8 {
64            self.data.push(self.current_byte);
65            self.current_byte = 0;
66            self.bit_count = 0;
67        }
68    }
69
70    /// Write multiple bits (up to 64)
71    fn write_bits(&mut self, value: u64, num_bits: u8) {
72        for i in 0..num_bits {
73            let bit = ((value >> (num_bits - 1 - i)) & 1) != 0;
74            self.write_bit(bit);
75        }
76    }
77
78    /// Finish writing (flush remaining bits)
79    fn finish(mut self) -> Vec<u8> {
80        self.align_byte();
81        self.data
82    }
83
84    /// Flush a partial byte so the next value starts on a byte boundary.
85    /// Independent encodings were concatenated this way.
86    fn align_byte(&mut self) {
87        if self.bit_count > 0 {
88            self.data.push(self.current_byte);
89            self.current_byte = 0;
90            self.bit_count = 0;
91        }
92    }
93
94    /// BIP158: Encode value x as:
95    /// - Write (x / 2^P) in unary (that many 1s, then a 0)
96    /// - Write (x mod 2^P) in binary (P bits)
97    fn write_golomb_rice(&mut self, value: u64, p: u8) {
98        let quotient = value >> p;
99        let remainder = value & ((1u64 << p) - 1);
100        for _ in 0..quotient {
101            self.write_bit(true);
102        }
103        self.write_bit(false);
104        self.write_bits(remainder, p);
105    }
106}
107
108/// Golomb-Rice encode one value. Tests use this as the separate-encoding oracle.
109#[cfg(test)]
110fn golomb_rice_encode(value: u64, p: u8) -> Vec<u8> {
111    let mut writer = BitWriter::new();
112    writer.write_golomb_rice(value, p);
113    writer.finish()
114}
115
116/// Golomb-Rice deltas from 0, one bitstream. The partial byte is flushed once.
117fn encode_sorted_differences(values: &[u64]) -> Vec<u8> {
118    let mut writer = BitWriter::new();
119    let mut prev = 0u64;
120    for value in values {
121        writer.write_golomb_rice(value.saturating_sub(prev), BIP158_P);
122        prev = *value;
123    }
124    writer.finish()
125}
126
127fn write_compact_size(n: u64, out: &mut Vec<u8>) {
128    if n < 253 {
129        out.push(n as u8);
130    } else if n <= u16::MAX as u64 {
131        out.push(0xfd);
132        out.extend_from_slice(&(n as u16).to_le_bytes());
133    } else if n <= u32::MAX as u64 {
134        out.push(0xfe);
135        out.extend_from_slice(&(n as u32).to_le_bytes());
136    } else {
137        out.push(0xff);
138        out.extend_from_slice(&n.to_le_bytes());
139    }
140}
141
142fn read_compact_size(data: &[u8]) -> Option<(u64, usize)> {
143    let first = *data.first()?;
144    match first {
145        n @ 0..=252 => Some((n as u64, 1)),
146        0xfd => {
147            let bytes = data.get(1..3)?;
148            Some((u16::from_le_bytes([bytes[0], bytes[1]]) as u64, 3))
149        }
150        0xfe => {
151            let bytes: [u8; 4] = data.get(1..5)?.try_into().ok()?;
152            Some((u32::from_le_bytes(bytes) as u64, 5))
153        }
154        _ => {
155            let bytes: [u8; 8] = data.get(1..9)?.try_into().ok()?;
156            Some((u64::from_le_bytes(bytes), 9))
157        }
158    }
159}
160
161/// Bit reader for Golomb-Rice decoding
162struct BitReader<'a> {
163    data: &'a [u8],
164    bit_offset: usize,
165}
166
167impl<'a> BitReader<'a> {
168    fn new(data: &'a [u8]) -> Self {
169        Self {
170            data,
171            bit_offset: 0,
172        }
173    }
174
175    /// Read a single bit
176    fn read_bit(&mut self) -> Option<bool> {
177        if self.bit_offset >= self.data.len() * 8 {
178            return None;
179        }
180        let byte_idx = self.bit_offset / 8;
181        let bit_idx = self.bit_offset % 8;
182        let bit = (self.data[byte_idx] >> (7 - bit_idx)) & 1;
183        self.bit_offset += 1;
184        Some(bit == 1)
185    }
186
187    /// Read P bits as a u64
188    fn read_bits(&mut self, p: u8) -> Option<u64> {
189        let mut value = 0u64;
190        for _ in 0..p {
191            if let Some(bit) = self.read_bit() {
192                value = (value << 1) | (if bit { 1 } else { 0 });
193            } else {
194                return None;
195            }
196        }
197        Some(value)
198    }
199}
200
201/// Golomb-Rice decode a value from stream
202///
203/// BIP158: Decode value x by:
204/// - Read unary-encoded quotient (count 1s until 0)
205/// - Read P bits as remainder
206/// - Value = quotient * 2^P + remainder
207fn golomb_rice_decode(reader: &mut BitReader, p: u8) -> Option<u64> {
208    // Read quotient in unary (count 1s until we hit a 0)
209    let mut quotient = 0u64;
210    loop {
211        match reader.read_bit() {
212            Some(true) => quotient += 1,
213            Some(false) => break,
214            None => return None,
215        }
216    }
217
218    // Read remainder in binary (P bits)
219    let remainder = reader.read_bits(p)?;
220
221    // Reconstruct value: quotient * 2^P + remainder
222    Some((quotient << p) | remainder)
223}
224
225/// Build a compact block filter from transaction data
226///
227/// BIP158: Filter contains:
228/// 1. All spendable output scriptPubKeys in the block
229/// 2. All scriptPubKeys from outputs spent by block's inputs
230pub fn build_block_filter(
231    block_hash: &[u8; 32],
232    block_transactions: &[Transaction],
233    previous_outpoint_scripts: &[Vec<u8>],
234) -> Result<CompactBlockFilter, String> {
235    let refs: Vec<&[u8]> = previous_outpoint_scripts
236        .iter()
237        .map(Vec::as_slice)
238        .collect();
239    build_block_filter_slices(block_hash, block_transactions, &refs)
240}
241
242/// Same filter as [`build_block_filter`]. The spent scripts are borrowed.
243pub fn build_block_filter_slices<'a>(
244    block_hash: &[u8; 32],
245    block_transactions: &'a [Transaction],
246    previous_outpoint_scripts: &[&'a [u8]],
247) -> Result<CompactBlockFilter, String> {
248    let mut output_scripts = Vec::new();
249    for tx in block_transactions {
250        for output in &tx.outputs {
251            output_scripts.push(output.script_pubkey.as_slice());
252        }
253    }
254    build_block_filter_output_scripts(block_hash, output_scripts, previous_outpoint_scripts)
255}
256
257/// Same filter as [`build_block_filter_slices`], from output scripts already in hand.
258///
259/// `output_scripts` is reused. Spent scripts are appended to it. Empty scripts are dropped.
260pub fn build_block_filter_output_scripts<'a>(
261    block_hash: &[u8; 32],
262    mut scripts: Vec<&'a [u8]>,
263    previous_outpoint_scripts: &[&'a [u8]],
264) -> Result<CompactBlockFilter, String> {
265    scripts.extend(previous_outpoint_scripts.iter().copied());
266    scripts.retain(|script| !script.is_empty() && script.first() != Some(&0x6a));
267    scripts.sort_unstable();
268    scripts.dedup();
269    let n = scripts.len() as u64;
270    if n == 0 {
271        // Compact-size 0. An empty byte string omits N.
272        return Ok(CompactBlockFilter {
273            filter_data: Arc::new(vec![0]),
274            num_elements: 0,
275        });
276    }
277
278    let (k0, k1) = filter_key(block_hash);
279    let f = (n as u128).saturating_mul(BIP158_M as u128);
280    let mut hashed_values: Vec<u64> = scripts
281        .iter()
282        .map(|script| map_script(k0, k1, script, f))
283        .collect();
284    hashed_values.sort_unstable();
285
286    let mut filter_data = Vec::new();
287    write_compact_size(n, &mut filter_data);
288    filter_data.extend(encode_sorted_differences(&hashed_values));
289
290    Ok(CompactBlockFilter {
291        filter_data: Arc::new(filter_data),
292        num_elements: n as u32,
293    })
294}
295
296/// Match a script against a compact block filter
297///
298/// Returns true if the script (or its hash) is likely in the filter.
299/// A filter that does not fully decode returns false.
300///
301/// Algorithm:
302/// 1. Map the script with the block-hash key and F = N * M
303/// 2. Read the compact-size count, then the Golomb-Rice differences
304/// 3. The script matches when its mapped value is one of those values
305pub fn match_filter(block_hash: &[u8; 32], filter: &CompactBlockFilter, script: &[u8]) -> bool {
306    if filter.num_elements == 0 {
307        return false;
308    }
309
310    let n = filter.num_elements as u64;
311    let Some((counted, offset)) = read_compact_size(filter.filter_data.as_slice()) else {
312        return false;
313    };
314    if counted != n {
315        return false;
316    }
317    let (k0, k1) = filter_key(block_hash);
318    let f = (n as u128).saturating_mul(BIP158_M as u128);
319    let script_hash = map_script(k0, k1, script, f);
320
321    let mut reader = BitReader::new(&filter.filter_data[offset..]);
322    let mut current_value = 0u64;
323    let mut found = false;
324    for _ in 0..filter.num_elements {
325        let Some(diff) = golomb_rice_decode(&mut reader, BIP158_P) else {
326            return false;
327        };
328        current_value += diff;
329        if current_value == script_hash {
330            found = true;
331        }
332    }
333    found
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    const BLOCK_HASH: [u8; 32] = [0x11; 32];
341
342    #[test]
343    fn one_script_encodes_the_mapped_value_and_ignores_op_return() {
344        let script = b"\x51";
345        let filter =
346            build_block_filter_output_scripts(&BLOCK_HASH, vec![script.as_slice()], &[]).unwrap();
347        let (k0, k1) = filter_key(&BLOCK_HASH);
348        let mapped = ((siphash24(k0, k1, script) as u128 * 784_931) >> 64) as u64;
349        let mut expected = Vec::new();
350        write_compact_size(1, &mut expected);
351        expected.extend(golomb_rice_encode(mapped, BIP158_P));
352        assert_eq!(filter.filter_data.as_slice(), expected.as_slice());
353        assert_eq!(filter.num_elements, 1);
354        assert!(match_filter(&BLOCK_HASH, &filter, script));
355
356        let with_return = build_block_filter_output_scripts(
357            &BLOCK_HASH,
358            vec![script.as_slice(), &[0x6a, 0x01]],
359            &[],
360        )
361        .unwrap();
362        assert_eq!(with_return.filter_data, filter.filter_data);
363    }
364
365    #[test]
366    fn test_golomb_rice_encode() {
367        // Test encoding small value
368        let encoded = golomb_rice_encode(0, BIP158_P);
369        assert!(!encoded.is_empty());
370
371        let encoded2 = golomb_rice_encode(1, BIP158_P);
372        assert!(!encoded2.is_empty());
373    }
374
375    #[test]
376    fn test_empty_filter() {
377        let filter = build_block_filter(&BLOCK_HASH, &[], &[]).unwrap();
378        assert_eq!(filter.num_elements, 0);
379        assert_eq!(filter.filter_data.as_slice(), &[0]);
380    }
381
382    #[test]
383    fn test_deltas_share_one_bitstream() {
384        let values = [0u64, 1, 100, 10_000];
385        let mut writer = BitWriter::new();
386        let mut prev = 0u64;
387        for value in values {
388            writer.write_golomb_rice(value - prev, BIP158_P);
389            prev = value;
390        }
391        assert_eq!(writer.finish(), encode_sorted_differences(&values));
392    }
393
394    #[test]
395    fn test_golomb_rice_encode_decode_roundtrip() {
396        let test_values = vec![0, 1, 2, 10, 100, 1000, 10000];
397
398        for value in test_values {
399            let encoded = golomb_rice_encode(value, BIP158_P);
400            let mut reader = BitReader::new(&encoded);
401            let decoded = golomb_rice_decode(&mut reader, BIP158_P);
402
403            assert_eq!(decoded, Some(value), "Roundtrip failed for value {value}");
404        }
405    }
406
407    #[test]
408    fn test_build_and_match_filter() {
409        use crate::{OutPoint, Transaction, TransactionInput, TransactionOutput};
410
411        // Create a test transaction
412        let tx = Transaction {
413            version: 1,
414            inputs: vec![TransactionInput {
415                prevout: OutPoint {
416                    hash: [0u8; 32],
417                    index: 0,
418                },
419                script_sig: vec![],
420                sequence: 0xffffffff,
421            }]
422            .into(),
423            outputs: vec![TransactionOutput {
424                value: 1000,
425                script_pubkey: vec![blvm_consensus::opcodes::OP_1, blvm_consensus::opcodes::OP_2],
426            }]
427            .into(),
428            lock_time: 0,
429        };
430
431        // Build filter
432        let filter = build_block_filter(&BLOCK_HASH, &[tx.clone()], &[]).unwrap();
433        assert!(filter.num_elements > 0);
434
435        // Match the script that's in the filter
436        let script_in_filter = &tx.outputs[0].script_pubkey;
437        assert!(match_filter(&BLOCK_HASH, &filter, script_in_filter));
438
439        // Match a script that's not in the filter
440        let script_not_in_filter = vec![0x53, 0x54]; // OP_3 OP_4
441        // Note: May have false positives due to GCS nature, but should generally work
442        let _matched = match_filter(&BLOCK_HASH, &filter, &script_not_in_filter);
443        // False positives are possible, so we can't assert false
444        // But we can verify the filter works for scripts that are definitely in it
445
446        // One more claimed element than the stream can decode rejects the filter.
447        let truncated = CompactBlockFilter {
448            filter_data: filter.filter_data.clone(),
449            num_elements: filter.num_elements + 1,
450        };
451        assert!(!match_filter(&BLOCK_HASH, &truncated, script_in_filter));
452    }
453
454    #[test]
455    fn test_filter_bytes_ignore_script_order() {
456        use crate::{Transaction, TransactionInput, TransactionOutput};
457
458        let tx = |scripts: &[&[u8]]| Transaction {
459            version: 1,
460            inputs: Vec::<TransactionInput>::new().into(),
461            outputs: scripts
462                .iter()
463                .map(|script| TransactionOutput {
464                    value: 1,
465                    script_pubkey: script.to_vec(),
466                })
467                .collect::<Vec<_>>()
468                .into(),
469            lock_time: 0,
470        };
471        let first =
472            build_block_filter(&BLOCK_HASH, &[tx(&[b"\x51\x52", b"\x53\x54"])], &[]).unwrap();
473        let second =
474            build_block_filter(&BLOCK_HASH, &[tx(&[b"\x53\x54", b"\x51\x52"])], &[]).unwrap();
475        assert_eq!(first, second);
476        assert_eq!(first.num_elements, 2);
477    }
478
479    /// Bytes from an independent SipHash-2-4 and one Golomb-Rice bitstream.
480    /// Block hash is 32 bytes of `0x11`. `P` is 19 and `F` is `N * 784_931`.
481    #[test]
482    fn known_filter_bytes_match_an_independent_encoding() {
483        let one = build_block_filter_output_scripts(&BLOCK_HASH, vec![b"\x51"], &[]).unwrap();
484        let expected = hex_bytes("01719c40");
485        assert_eq!(one.filter_data.as_slice(), expected.as_slice());
486        assert_eq!(one.num_elements, 1);
487        assert!(match_filter(&BLOCK_HASH, &one, b"\x51"));
488        assert!(!match_filter(&BLOCK_HASH, &one, b"\x52"));
489
490        let with_return =
491            build_block_filter_output_scripts(&BLOCK_HASH, vec![b"\x51", &[0x6a, 0x01]], &[])
492                .unwrap();
493        assert_eq!(with_return.filter_data.as_slice(), expected.as_slice());
494
495        let duplicated =
496            build_block_filter_output_scripts(&BLOCK_HASH, vec![b"\x51", b"\x51"], &[]).unwrap();
497        assert_eq!(duplicated.filter_data.as_slice(), expected.as_slice());
498
499        let two =
500            build_block_filter_output_scripts(&BLOCK_HASH, vec![b"\x52", b"\x51"], &[]).unwrap();
501        assert_eq!(
502            two.filter_data.as_slice(),
503            hex_bytes("02b19c4999d800").as_slice()
504        );
505        assert_eq!(two.num_elements, 2);
506        assert!(match_filter(&BLOCK_HASH, &two, b"\x51"));
507        assert!(match_filter(&BLOCK_HASH, &two, b"\x52"));
508
509        let collided = build_block_filter_output_scripts(
510            &BLOCK_HASH,
511            vec![&[0x25, 0x01, 0x00], &[0x2e, 0x08, 0x00]],
512            &[],
513        )
514        .unwrap();
515        assert_eq!(collided.num_elements, 2);
516        assert_eq!(
517            collided.filter_data.as_slice(),
518            hex_bytes("0207aea00000").as_slice()
519        );
520        assert!(match_filter(&BLOCK_HASH, &collided, &[0x25, 0x01, 0x00]));
521        assert!(match_filter(&BLOCK_HASH, &collided, &[0x2e, 0x08, 0x00]));
522
523        let empty = build_block_filter_output_scripts(&BLOCK_HASH, vec![], &[]).unwrap();
524        assert_eq!(empty.filter_data.as_slice(), &[0]);
525        assert!(!match_filter(&BLOCK_HASH, &empty, b"\x51"));
526        let only_return =
527            build_block_filter_output_scripts(&BLOCK_HASH, vec![&[0x6a]], &[]).unwrap();
528        assert_eq!(only_return.filter_data.as_slice(), &[0]);
529
530        // Quotient 1. Mapped value 730006 is 1 * 2^19 + 205718.
531        let unary =
532            build_block_filter_output_scripts(&BLOCK_HASH, vec![&[0x04, 0x00]], &[]).unwrap();
533        assert_eq!(
534            unary.filter_data.as_slice(),
535            hex_bytes("01991cb0").as_slice()
536        );
537        assert!(match_filter(&BLOCK_HASH, &unary, &[0x04, 0x00]));
538
539        // Eight scripts. Deltas have quotients 2, 1, 0, 2, 3, 0, 0, 0.
540        let scripts: [&[u8]; 8] = [
541            &[0x01, 0x00, 0x5a],
542            &[0x02, 0x00, 0x5a],
543            &[0x03, 0x00, 0x5a],
544            &[0x04, 0x00, 0x5a],
545            &[0x05, 0x00, 0x5a],
546            &[0x06, 0x00, 0x5a],
547            &[0x07, 0x00, 0x5a],
548            &[0x08, 0x00, 0x5a],
549        ];
550        let wide = build_block_filter_output_scripts(&BLOCK_HASH, scripts.to_vec(), &[]).unwrap();
551        assert_eq!(wide.num_elements, 8);
552        assert_eq!(
553            wide.filter_data.as_slice(),
554            hex_bytes("08c09d02f1a544977793c53f77dc52be4722d7e3911d").as_slice()
555        );
556        for script in scripts {
557            assert!(match_filter(&BLOCK_HASH, &wide, script));
558        }
559    }
560
561    fn hex_bytes(hex: &str) -> Vec<u8> {
562        (0..hex.len())
563            .step_by(2)
564            .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
565            .collect()
566    }
567}