Skip to main content

blvm_protocol/
network.rs

1//! Bitcoin P2P Network Protocol (Orange Paper Section 10)
2//!
3//! This module provides Bitcoin P2P protocol message types and processing.
4//! Protocol-specific limits and validation are handled here, with consensus
5//! validation delegated to the consensus layer.
6
7use crate::bip152::ShortTxId;
8use crate::error::ProtocolError;
9use crate::validation::ProtocolValidationContext;
10use crate::{BitcoinProtocolEngine, ProtocolConfig, Result};
11use blvm_consensus::error::ConsensusError;
12use blvm_consensus::types::UtxoSet;
13use blvm_consensus::types::{Block, BlockHeader, Hash, Transaction, ValidationResult};
14use std::sync::Arc;
15use thiserror::Error;
16
17// Commons module is always available (ban list sharing doesn't require utxo-commitments)
18pub mod commons {
19    pub use crate::commons::*;
20}
21
22// BIP324: v2 encrypted transport
23#[cfg(all(
24    feature = "bip324",
25    any(target_arch = "x86_64", target_arch = "aarch64")
26))]
27pub mod v2_transport {
28    pub use crate::v2_transport::*;
29}
30
31#[cfg(test)]
32mod bip155_tests;
33
34/// NetworkMessage: Bitcoin P2P protocol message types
35///
36/// Network message types for Bitcoin P2P protocol
37#[derive(Debug, Clone, PartialEq)]
38pub enum NetworkMessage {
39    Version(VersionMessage),
40    VerAck,
41    Addr(AddrMessage),
42    AddrV2(AddrV2Message), // BIP155: Extended address format
43    Inv(InvMessage),
44    GetData(GetDataMessage),
45    GetHeaders(GetHeadersMessage),
46    Headers(HeadersMessage),
47    /// Full block message.  Witnesses are required for correct SegWit/Taproot validation and
48    /// wire round-tripping.  The second field is the per-tx, per-input witness stack.
49    Block(Arc<Block>, Vec<Vec<blvm_consensus::segwit::Witness>>),
50    /// Transaction message. The second field is the per-input witness stack.
51    /// A legacy transaction carries an empty stack list.
52    Tx(Arc<Transaction>, Vec<crate::segwit::Witness>),
53    Ping(PingMessage),
54    Pong(PongMessage),
55    MemPool,
56    FeeFilter(FeeFilterMessage),
57    // Additional core P2P messages
58    GetBlocks(GetBlocksMessage),
59    GetAddr,
60    NotFound(NotFoundMessage),
61    Reject(RejectMessage),
62    SendHeaders,
63    // BIP152 Compact Block Relay
64    SendCmpct(SendCmpctMessage),
65    CmpctBlock(CmpctBlockMessage),
66    GetBlockTxn(GetBlockTxnMessage),
67    BlockTxn(BlockTxnMessage),
68    // Commons-specific protocol extensions
69    #[cfg(feature = "utxo-commitments")]
70    GetUTXOSet(commons::GetUTXOSetMessage),
71    #[cfg(feature = "utxo-commitments")]
72    UTXOSet(commons::UTXOSetMessage),
73    #[cfg(feature = "utxo-commitments")]
74    GetFilteredBlock(commons::GetFilteredBlockMessage),
75    #[cfg(feature = "utxo-commitments")]
76    FilteredBlock(commons::FilteredBlockMessage),
77    GetBanList(commons::GetBanListMessage),
78    BanList(commons::BanListMessage),
79}
80
81/// Version message for initial handshake
82#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
83pub struct VersionMessage {
84    pub version: i32,
85    pub services: u64,
86    pub timestamp: i64,
87    pub addr_recv: NetworkAddress,
88    pub addr_from: NetworkAddress,
89    pub nonce: u64,
90    pub user_agent: String,
91    pub start_height: i32,
92    pub relay: bool,
93}
94
95/// Block P2P message: consensus block paired with its segwit witness stack.
96///
97/// The consensus [`Block`] carries transactions without witness data; witnesses are kept
98/// alongside so the node can round-trip the full segwit wire format without altering the
99/// consensus type.
100#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
101pub struct BlockMessage {
102    pub block: Block,
103    /// One `Vec<Witness>` per transaction, one `Witness` per input.
104    #[serde(skip_serializing_if = "Vec::is_empty", default)]
105    pub witnesses: Vec<Vec<blvm_consensus::segwit::Witness>>,
106}
107
108/// Transaction P2P message (thin wrapper for dispatch).
109#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
110pub struct TxMessage {
111    pub transaction: Transaction,
112    /// One witness stack per input. Empty when the wire encoding has no witness flag.
113    #[serde(skip_serializing_if = "Vec::is_empty", default)]
114    pub witnesses: Vec<crate::segwit::Witness>,
115}
116
117/// Compact block P2P message: decoded BIP152 compact block.
118///
119/// Holds the [`crate::bip152::CompactBlock`] abstraction used for efficient block relay.
120/// The node converts between this and the wire [`CmpctBlockMessage`] on ingress/egress.
121#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
122pub struct CompactBlockMessage {
123    pub compact_block: crate::bip152::CompactBlock,
124}
125
126/// Address message containing peer addresses
127#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
128pub struct AddrMessage {
129    pub addresses: Vec<NetworkAddress>,
130}
131
132/// Inventory message listing available objects
133#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
134pub struct InvMessage {
135    pub inventory: Vec<InventoryVector>,
136}
137
138/// GetData message requesting specific objects
139#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
140pub struct GetDataMessage {
141    pub inventory: Vec<InventoryVector>,
142}
143
144/// GetHeaders message requesting block headers
145#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
146pub struct GetHeadersMessage {
147    pub version: u32,
148    pub block_locator_hashes: Vec<Hash>,
149    pub hash_stop: Hash,
150}
151
152/// Headers message containing block headers
153#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
154pub struct HeadersMessage {
155    pub headers: Vec<BlockHeader>,
156}
157
158/// Ping message for connection keepalive
159#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
160pub struct PingMessage {
161    pub nonce: u64,
162}
163
164/// Pong message responding to ping
165#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
166pub struct PongMessage {
167    pub nonce: u64,
168}
169
170/// FeeFilter message setting minimum fee rate
171#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
172pub struct FeeFilterMessage {
173    pub feerate: u64,
174}
175
176/// GetBlocks message requesting blocks by locator
177#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
178pub struct GetBlocksMessage {
179    pub version: u32,
180    pub block_locator_hashes: Vec<Hash>,
181    pub hash_stop: Hash,
182}
183
184/// NotFound message indicating requested object not found
185#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
186pub struct NotFoundMessage {
187    pub inventory: Vec<InventoryVector>,
188}
189
190/// Reject message rejecting a message with reason
191#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
192pub struct RejectMessage {
193    pub message: String,          // Command name of rejected message
194    pub code: u8, // Rejection code (0x01=malformed, 0x10=invalid, 0x11=obsolete, 0x12=duplicate, 0x40=nonstandard, 0x41=dust, 0x42=insufficientfee, 0x43=checkpoint)
195    pub reason: String, // Human-readable reason
196    pub extra_data: Option<Hash>, // Optional hash for rejected object
197}
198
199/// SendCmpct message - Negotiate compact block relay support (BIP152)
200#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
201pub struct SendCmpctMessage {
202    /// Compact block version (1 or 2)
203    pub version: u64,
204    /// Whether to prefer compact blocks (1) or regular blocks (0)
205    pub prefer_cmpct: u8,
206}
207
208/// CmpctBlock message - Compact block data (BIP152)
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct CmpctBlockMessage {
211    /// Block header
212    pub header: BlockHeader,
213    /// Nonce for short transaction ID calculation (SipHash key derivation)
214    pub nonce: u64,
215    /// Short transaction IDs (6 bytes each)
216    pub short_ids: Vec<ShortTxId>,
217    /// Prefilled transactions (transactions that are likely missing)
218    pub prefilled_txs: Vec<PrefilledTransaction>,
219}
220
221/// PrefilledTransaction - Transaction included in compact block
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct PrefilledTransaction {
224    /// Index in block (0 = coinbase)
225    pub index: u16,
226    /// Transaction data
227    pub tx: Transaction,
228    /// Witness data (one stack per input). If Some and non-empty, serializes with TX_WITH_WITNESS per BIP152/Core.
229    pub witness: Option<Vec<blvm_consensus::segwit::Witness>>,
230}
231
232/// Error converting between [`crate::bip152::CompactBlock`] and wire [`CmpctBlockMessage`].
233#[derive(Debug, Clone, PartialEq, Eq, Error)]
234pub enum CompactBlockWireConvertError {
235    #[error("prefilled transaction index {0} does not fit in BIP152 u16 index")]
236    PrefilledIndexTooLarge(usize),
237    #[error("duplicate prefilled transaction index {0}")]
238    DuplicatePrefilledIndex(usize),
239}
240
241impl TryFrom<crate::bip152::CompactBlock> for CmpctBlockMessage {
242    type Error = CompactBlockWireConvertError;
243
244    /// Builds a cmpctblock-shaped message from the integration [`crate::bip152::CompactBlock`].
245    ///
246    /// Prefilled entries are **sorted by transaction index** (required for BIP152 diff-encoding on the wire).
247    /// Witness stacks stored on the compact block are copied onto each prefilled transaction.
248    fn try_from(mut value: crate::bip152::CompactBlock) -> std::result::Result<Self, Self::Error> {
249        value.prefilled_txs.sort_by_key(|(i, _, _)| *i);
250        let mut prefilled_txs = Vec::with_capacity(value.prefilled_txs.len());
251        let mut prev_idx: Option<usize> = None;
252        for (idx, tx, witness) in value.prefilled_txs {
253            if prev_idx == Some(idx) {
254                return Err(CompactBlockWireConvertError::DuplicatePrefilledIndex(idx));
255            }
256            prev_idx = Some(idx);
257            let index = u16::try_from(idx)
258                .map_err(|_| CompactBlockWireConvertError::PrefilledIndexTooLarge(idx))?;
259            prefilled_txs.push(PrefilledTransaction { index, tx, witness });
260        }
261        Ok(CmpctBlockMessage {
262            header: value.header,
263            nonce: value.nonce,
264            short_ids: value.short_ids,
265            prefilled_txs,
266        })
267    }
268}
269
270impl From<&CmpctBlockMessage> for crate::bip152::CompactBlock {
271    fn from(msg: &CmpctBlockMessage) -> Self {
272        Self {
273            header: msg.header.clone(),
274            nonce: msg.nonce,
275            short_ids: msg.short_ids.clone(),
276            prefilled_txs: msg
277                .prefilled_txs
278                .iter()
279                .map(|p| (usize::from(p.index), p.tx.clone(), p.witness.clone()))
280                .collect(),
281        }
282    }
283}
284
285impl From<CmpctBlockMessage> for crate::bip152::CompactBlock {
286    fn from(msg: CmpctBlockMessage) -> Self {
287        Self {
288            header: msg.header,
289            nonce: msg.nonce,
290            short_ids: msg.short_ids,
291            prefilled_txs: msg
292                .prefilled_txs
293                .into_iter()
294                .map(|p| (usize::from(p.index), p.tx, p.witness))
295                .collect(),
296        }
297    }
298}
299
300/// GetBlockTxn message - Request missing transactions from compact block (BIP152)
301#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
302pub struct GetBlockTxnMessage {
303    /// Block hash for the compact block
304    pub block_hash: Hash,
305    /// Indices of transactions to request (0-indexed)
306    pub indices: Vec<u16>,
307}
308
309/// BlockTxn message - Response with requested transactions (BIP152)
310#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
311pub struct BlockTxnMessage {
312    /// Block hash for the compact block
313    pub block_hash: Hash,
314    /// Requested transactions in order
315    pub transactions: Vec<Transaction>,
316    /// Witness data (one Vec<Witness> per tx, one Witness per input). If Some and len matches transactions, serializes with TX_WITH_WITNESS per BIP152/Core.
317    pub witnesses: Option<Vec<Vec<blvm_consensus::segwit::Witness>>>,
318}
319
320/// Network address structure (legacy format)
321#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
322pub struct NetworkAddress {
323    pub services: u64,
324    pub ip: [u8; 16], // IPv6 address (IPv4 mapped to IPv6)
325    pub port: u16,
326    /// Advertised unix time. Zero is stored and is outside the relay horizon.
327    pub time: u32,
328}
329
330/// BIP155: Address type for addrv2 message
331#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
332#[repr(u8)]
333pub enum AddressType {
334    IPv4 = 1,
335    IPv6 = 2,
336    TorV2 = 3,
337    TorV3 = 4,
338    I2P = 5,
339    CJDNS = 6,
340}
341
342impl AddressType {
343    /// Get the expected address length in bytes for this type
344    pub fn address_length(&self) -> usize {
345        match self {
346            AddressType::IPv4 => 4,
347            AddressType::IPv6 => 16,
348            AddressType::TorV2 => 10,
349            AddressType::TorV3 => 32,
350            AddressType::I2P => 32,
351            AddressType::CJDNS => 16,
352        }
353    }
354
355    /// Try to create AddressType from u8
356    pub fn from_u8(value: u8) -> Option<Self> {
357        match value {
358            1 => Some(AddressType::IPv4),
359            2 => Some(AddressType::IPv6),
360            3 => Some(AddressType::TorV2),
361            4 => Some(AddressType::TorV3),
362            5 => Some(AddressType::I2P),
363            6 => Some(AddressType::CJDNS),
364            _ => None,
365        }
366    }
367}
368
369/// BIP155: Extended address message (addrv2)
370#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
371pub struct AddrV2Message {
372    pub addresses: Vec<NetworkAddressV2>,
373}
374
375/// BIP155: Extended network address structure
376#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
377pub struct NetworkAddressV2 {
378    pub time: u32,
379    pub services: u64,
380    pub address_type: AddressType,
381    pub address: Vec<u8>, // Variable length based on address_type
382    pub port: u16,
383}
384
385impl NetworkAddressV2 {
386    /// Create a new NetworkAddressV2
387    pub fn new(
388        time: u32,
389        services: u64,
390        address_type: AddressType,
391        address: Vec<u8>,
392        port: u16,
393    ) -> Result<Self> {
394        let expected_len = address_type.address_length();
395        if address.len() != expected_len {
396            return Err(ProtocolError::Consensus(ConsensusError::Serialization(
397                std::borrow::Cow::Owned(format!(
398                    "Invalid address length for type {:?}: expected {}, got {}",
399                    address_type,
400                    expected_len,
401                    address.len()
402                )),
403            )));
404        }
405        Ok(Self {
406            time,
407            services,
408            address_type,
409            address,
410            port,
411        })
412    }
413
414    /// Convert to legacy NetworkAddress (if possible)
415    pub fn to_legacy(&self) -> Option<NetworkAddress> {
416        match self.address_type {
417            AddressType::IPv4 => {
418                if self.address.len() == 4 {
419                    // Map IPv4 to IPv6-mapped format
420                    let mut ipv6 = [0u8; 16];
421                    ipv6[10] = 0xff;
422                    ipv6[11] = 0xff;
423                    ipv6[12..16].copy_from_slice(&self.address);
424                    Some(NetworkAddress {
425                        time: self.time,
426                        services: self.services,
427                        ip: ipv6,
428                        port: self.port,
429                    })
430                } else {
431                    None
432                }
433            }
434            AddressType::IPv6 => {
435                if self.address.len() == 16 {
436                    let mut ipv6 = [0u8; 16];
437                    ipv6.copy_from_slice(&self.address);
438                    Some(NetworkAddress {
439                        time: self.time,
440                        services: self.services,
441                        ip: ipv6,
442                        port: self.port,
443                    })
444                } else {
445                    None
446                }
447            }
448            _ => None, // Tor, I2P, CJDNS cannot be converted to legacy format
449        }
450    }
451}
452
453/// Inventory vector identifying objects
454#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
455pub struct InventoryVector {
456    pub inv_type: u32,
457    pub hash: Hash,
458}
459
460/// Network response to a message
461#[derive(Debug, Clone)]
462pub enum NetworkResponse {
463    Ok,
464    SendMessage(Box<NetworkMessage>),
465    SendMessages(Vec<NetworkMessage>),
466    Reject(String),
467}
468
469/// Compact block held until `blocktxn` fills the short ids the mempool missed.
470#[derive(Clone)]
471pub(crate) struct PendingCompact {
472    pub header: BlockHeader,
473    pub nonce: u64,
474    pub slots: Vec<Option<(Transaction, Vec<blvm_consensus::segwit::Witness>)>>,
475    pub missing: Vec<(usize, ShortTxId)>,
476}
477
478/// Peer connection state
479#[derive(Clone)]
480pub struct PeerState {
481    pub version: i32,
482    pub services: u64,
483    pub user_agent: String,
484    pub start_height: i32,
485    pub handshake_complete: bool,
486    pub known_addresses: Vec<NetworkAddress>,
487    pub ping_nonce: Option<u64>,
488    pub last_pong: Option<std::time::SystemTime>,
489    /// BIP133 minimum, in satoshis per 1,000 virtual bytes. `None` means the peer sent no filter.
490    pub min_fee_rate: Option<u64>,
491    /// BIP37 relay byte. `false` means this peer asked not to receive transaction announcements.
492    /// A version message that omits the byte leaves this `true`.
493    pub relay_tx: bool,
494    /// BIP130: peer prefers `headers` over `inv` for block announcements.
495    pub prefer_headers: bool,
496    /// BIP152: negotiated compact-block version (1 or 2), if peer sent sendcmpct.
497    pub compact_block_version: Option<u8>,
498    /// BIP152: peer prefers compact blocks when true.
499    pub prefer_compact_block: bool,
500    /// Compact block waiting on `blocktxn` for the short ids the mempool did not fill.
501    pub(crate) pending_compact: Option<PendingCompact>,
502    /// BIP324: v2 encrypted transport state (if enabled)
503    /// Note: V2Transport is not Clone, so we use Option<Box<V2Transport>> for storage
504    #[cfg(all(
505        feature = "bip324",
506        any(target_arch = "x86_64", target_arch = "aarch64")
507    ))]
508    pub v2_transport: Option<std::sync::Arc<crate::v2_transport::V2Transport>>,
509    /// BIP324: Whether v2 transport handshake is in progress
510    #[cfg(all(
511        feature = "bip324",
512        any(target_arch = "x86_64", target_arch = "aarch64")
513    ))]
514    pub v2_handshake: Option<std::sync::Arc<crate::v2_transport::V2Handshake>>,
515}
516
517impl PeerState {
518    pub fn new() -> Self {
519        Self {
520            version: 0,
521            services: 0,
522            user_agent: String::new(),
523            start_height: 0,
524            handshake_complete: false,
525            known_addresses: Vec::new(),
526            ping_nonce: None,
527            last_pong: None,
528            min_fee_rate: None,
529            relay_tx: true,
530            prefer_headers: false,
531            compact_block_version: None,
532            prefer_compact_block: false,
533            pending_compact: None,
534            #[cfg(all(
535                feature = "bip324",
536                any(target_arch = "x86_64", target_arch = "aarch64")
537            ))]
538            v2_transport: None,
539            #[cfg(all(
540                feature = "bip324",
541                any(target_arch = "x86_64", target_arch = "aarch64")
542            ))]
543            v2_handshake: None,
544        }
545    }
546
547    /// Check if peer supports BIP324 v2 encrypted transport
548    #[cfg(all(
549        feature = "bip324",
550        any(target_arch = "x86_64", target_arch = "aarch64")
551    ))]
552    pub fn supports_v2_transport(&self) -> bool {
553        use crate::service_flags::{has_flag, standard};
554        has_flag(self.services, standard::NODE_V2_TRANSPORT)
555    }
556
557    /// Check if v2 transport is active
558    #[cfg(all(
559        feature = "bip324",
560        any(target_arch = "x86_64", target_arch = "aarch64")
561    ))]
562    pub fn is_v2_transport_active(&self) -> bool {
563        self.v2_transport.is_some()
564    }
565}
566
567impl Default for PeerState {
568    fn default() -> Self {
569        Self::new()
570    }
571}
572
573/// Chain object (block or transaction)
574#[derive(Debug, Clone)]
575pub enum ChainObject {
576    Block(Arc<Block>),
577    Transaction(Arc<Transaction>),
578}
579
580impl ChainObject {
581    pub fn as_block(&self) -> Option<&Arc<Block>> {
582        match self {
583            ChainObject::Block(block) => Some(block),
584            _ => None,
585        }
586    }
587
588    pub fn as_transaction(&self) -> Option<&Arc<Transaction>> {
589        match self {
590            ChainObject::Transaction(tx) => Some(tx),
591            _ => None,
592        }
593    }
594}
595
596/// Trait for chain state access (node layer implements this)
597///
598/// This trait allows the protocol layer to query chain state without
599/// owning it. The node layer provides real implementations using its
600/// storage modules (BlockStore, TxIndex, MempoolManager).
601pub trait ChainStateAccess {
602    /// Check if we have an object (block or transaction) by hash
603    fn has_object(&self, hash: &Hash) -> bool;
604
605    /// Get an object (block or transaction) by hash
606    fn get_object(&self, hash: &Hash) -> Option<ChainObject>;
607
608    /// Get headers for a block locator (for GetHeaders requests)
609    /// This implements the Bitcoin block locator algorithm
610    fn get_headers_for_locator(&self, locator: &[Hash], stop: &Hash) -> Vec<BlockHeader>;
611
612    /// Get all mempool transactions
613    fn get_mempool_transactions(&self) -> Vec<Transaction>;
614
615    /// Witness stacks for a stored block (SegWit relay). Default: not available.
616    fn get_block_witnesses(
617        &self,
618        _hash: &Hash,
619    ) -> Option<Vec<Vec<blvm_consensus::segwit::Witness>>> {
620        None
621    }
622
623    /// Ban entries this node would return from `getbanlist`. Default: none stored.
624    fn ban_entries(&self) -> Vec<crate::commons::BanEntry> {
625        Vec::new()
626    }
627
628    /// Apply one ban the way `setban` does: the address is the IP with port 0, and
629    /// `timestamp` is the absolute unban time (`0` is permanent).
630    fn apply_ban(&self, entry: &crate::commons::BanEntry) -> std::result::Result<(), String> {
631        let _ = entry;
632        Err("ban path is not available".into())
633    }
634
635    /// Stored UTXO commitment for a block, when this node has one.
636    fn stored_utxo_commitment(
637        &self,
638        _block_hash: &Hash,
639    ) -> std::result::Result<Option<crate::commons::UTXOCommitment>, String> {
640        Ok(None)
641    }
642
643    /// Basic block filter for a block, the bytes `getblockfilter` already builds.
644    fn stored_block_filter(
645        &self,
646        _block_hash: &Hash,
647    ) -> std::result::Result<Option<(BlockHeader, Vec<u8>)>, String> {
648        Ok(None)
649    }
650}
651
652/// Process incoming network message
653///
654/// This function handles Bitcoin P2P protocol messages, applying protocol-specific
655/// limits and delegating consensus validation to the protocol engine.
656///
657/// # Arguments
658///
659/// * `engine` - The protocol engine (contains consensus layer)
660/// * `message` - The network message to process
661/// * `peer_state` - Current peer connection state
662/// * `chain_access` - Optional chain state access (node layer provides this)
663/// * `utxo_set` - Optional UTXO set for block validation
664/// * `height` - Optional block height for validation context
665///
666/// # Returns
667///
668/// A `NetworkResponse` indicating the result of processing
669pub fn process_network_message(
670    engine: &BitcoinProtocolEngine,
671    message: &NetworkMessage,
672    peer_state: &mut PeerState,
673    chain_access: Option<&dyn ChainStateAccess>,
674    utxo_set: Option<&UtxoSet>,
675    height: Option<u64>,
676) -> Result<NetworkResponse> {
677    let config = engine.get_config();
678    match message {
679        NetworkMessage::Version(version) => process_version_message(version, peer_state, config),
680        NetworkMessage::VerAck => process_verack_message(peer_state),
681        NetworkMessage::Addr(addr) => process_addr_message(addr, peer_state, config),
682        NetworkMessage::AddrV2(addrv2) => process_addrv2_message(addrv2, peer_state, config),
683        NetworkMessage::Inv(inv) => process_inv_message(inv, chain_access, config),
684        NetworkMessage::GetData(getdata) => process_getdata_message(getdata, chain_access, config),
685        NetworkMessage::GetHeaders(getheaders) => {
686            process_getheaders_message(getheaders, chain_access, config)
687        }
688        NetworkMessage::Headers(headers) => process_headers_message(headers, config),
689        NetworkMessage::Block(block, witnesses) => {
690            process_block_message(engine, block, witnesses, utxo_set, height, config)
691        }
692        NetworkMessage::Tx(tx, witnesses) => process_tx_message(engine, tx, witnesses, height),
693        NetworkMessage::Ping(ping) => process_ping_message(ping, peer_state),
694        NetworkMessage::Pong(pong) => process_pong_message(pong, peer_state),
695        NetworkMessage::MemPool => process_mempool_message(chain_access),
696        NetworkMessage::FeeFilter(feefilter) => process_feefilter_message(feefilter, peer_state),
697        NetworkMessage::GetBlocks(getblocks) => {
698            process_getblocks_message(getblocks, chain_access, config)
699        }
700        NetworkMessage::GetAddr => process_getaddr_message(peer_state, config),
701        NetworkMessage::NotFound(notfound) => process_notfound_message(notfound, config),
702        NetworkMessage::Reject(reject) => process_reject_message(reject, config),
703        NetworkMessage::SendHeaders => process_sendheaders_message(peer_state),
704        NetworkMessage::SendCmpct(sendcmpct) => {
705            process_sendcmpct_message(sendcmpct, peer_state, config)
706        }
707        NetworkMessage::CmpctBlock(cmpctblock) => process_cmpctblock_message(
708            engine,
709            cmpctblock,
710            peer_state,
711            chain_access,
712            utxo_set,
713            height,
714            config,
715        ),
716        NetworkMessage::GetBlockTxn(getblocktxn) => {
717            process_getblocktxn_message(getblocktxn, chain_access, config)
718        }
719        NetworkMessage::BlockTxn(blocktxn) => {
720            process_blocktxn_message(engine, blocktxn, peer_state, utxo_set, height, config)
721        }
722        #[cfg(feature = "utxo-commitments")]
723        NetworkMessage::GetUTXOSet(getutxoset) => {
724            process_getutxoset_message(getutxoset, chain_access)
725        }
726        #[cfg(feature = "utxo-commitments")]
727        NetworkMessage::UTXOSet(utxoset) => process_utxoset_message(utxoset, chain_access),
728        #[cfg(feature = "utxo-commitments")]
729        NetworkMessage::GetFilteredBlock(getfiltered) => {
730            process_getfilteredblock_message(getfiltered, chain_access)
731        }
732        #[cfg(feature = "utxo-commitments")]
733        NetworkMessage::FilteredBlock(filtered) => {
734            process_filteredblock_message(filtered, chain_access)
735        }
736        NetworkMessage::GetBanList(getbanlist) => {
737            process_getbanlist_message(getbanlist, chain_access)
738        }
739        NetworkMessage::BanList(banlist) => process_banlist_message(banlist, chain_access),
740    }
741}
742
743/// Process version message
744fn process_version_message(
745    version: &VersionMessage,
746    peer_state: &mut PeerState,
747    config: &ProtocolConfig,
748) -> Result<NetworkResponse> {
749    // Validate version message
750    if version.version < 70001 {
751        return Ok(NetworkResponse::Reject("Version too old".into()));
752    }
753
754    // Validate user agent length (from config)
755    if version.user_agent.len() > config.network_limits.max_user_agent_length {
756        return Ok(NetworkResponse::Reject(format!(
757            "User agent too long (max {} bytes)",
758            config.network_limits.max_user_agent_length
759        )));
760    }
761
762    // Update peer state
763    peer_state.version = version.version;
764    peer_state.services = version.services;
765    peer_state.user_agent = version.user_agent.clone();
766    peer_state.start_height = version.start_height;
767
768    // BIP324: Check if peer supports v2 transport and we should negotiate it
769    #[cfg(all(
770        feature = "bip324",
771        any(target_arch = "x86_64", target_arch = "aarch64")
772    ))]
773    {
774        use crate::service_flags::{has_flag, standard};
775        let peer_supports_v2 = has_flag(version.services, standard::NODE_V2_TRANSPORT);
776        let we_support_v2 = config.service_flags.node_v2_transport;
777
778        if peer_supports_v2 && we_support_v2 {
779            // Initiate v2 handshake (responder side)
780            let handshake = crate::v2_transport::V2Handshake::new_responder();
781            peer_state.v2_handshake = Some(std::sync::Arc::new(handshake));
782            // Note: Actual handshake happens at connection level, not in version message
783            // This just records that we should use v2 transport
784        }
785    }
786
787    // Send verack response
788    Ok(NetworkResponse::SendMessage(Box::new(
789        NetworkMessage::VerAck,
790    )))
791}
792
793/// Process verack message
794fn process_verack_message(peer_state: &mut PeerState) -> Result<NetworkResponse> {
795    peer_state.handshake_complete = true;
796    Ok(NetworkResponse::Ok)
797}
798
799/// Process addr message
800fn process_addr_message(
801    addr: &AddrMessage,
802    peer_state: &mut PeerState,
803    config: &ProtocolConfig,
804) -> Result<NetworkResponse> {
805    // Validate address count (from config)
806    if addr.addresses.len() > config.network_limits.max_addr_addresses {
807        return Ok(NetworkResponse::Reject(format!(
808            "Too many addresses (max {})",
809            config.network_limits.max_addr_addresses
810        )));
811    }
812
813    // Store addresses for future use
814    peer_state.known_addresses.extend(addr.addresses.clone());
815
816    Ok(NetworkResponse::Ok)
817}
818
819/// Process addrv2 message (BIP155)
820fn process_addrv2_message(
821    addrv2: &AddrV2Message,
822    peer_state: &mut PeerState,
823    config: &ProtocolConfig,
824) -> Result<NetworkResponse> {
825    // Validate address count (from config)
826    if addrv2.addresses.len() > config.network_limits.max_addr_addresses {
827        return Ok(NetworkResponse::Reject(format!(
828            "Too many addresses (max {})",
829            config.network_limits.max_addr_addresses
830        )));
831    }
832
833    // Convert addrv2 addresses to legacy format where possible and store
834    for addr_v2 in &addrv2.addresses {
835        if let Some(legacy_addr) = addr_v2.to_legacy() {
836            peer_state.known_addresses.push(legacy_addr);
837        }
838        // Note: Tor v3, I2P, CJDNS addresses cannot be converted to legacy format
839        // but we still process them (they're stored in addrv2 format in node layer)
840    }
841
842    Ok(NetworkResponse::Ok)
843}
844
845/// Process inv message
846fn process_inv_message(
847    inv: &InvMessage,
848    chain_access: Option<&dyn ChainStateAccess>,
849    config: &ProtocolConfig,
850) -> Result<NetworkResponse> {
851    // Validate inventory count (from config)
852    if inv.inventory.len() > config.network_limits.max_inv_items {
853        return Ok(NetworkResponse::Reject(format!(
854            "Too many inventory items (max {})",
855            config.network_limits.max_inv_items
856        )));
857    }
858
859    // Check which items we need (if chain access provided)
860    if let Some(chain) = chain_access {
861        let mut needed_items = Vec::with_capacity(inv.inventory.len());
862        for item in &inv.inventory {
863            if !chain.has_object(&item.hash) {
864                needed_items.push(item.clone());
865            }
866        }
867
868        if !needed_items.is_empty() {
869            return Ok(NetworkResponse::SendMessage(Box::new(
870                NetworkMessage::GetData(GetDataMessage {
871                    inventory: needed_items,
872                }),
873            )));
874        }
875    }
876
877    Ok(NetworkResponse::Ok)
878}
879
880/// Process getdata message
881fn process_getdata_message(
882    getdata: &GetDataMessage,
883    chain_access: Option<&dyn ChainStateAccess>,
884    config: &ProtocolConfig,
885) -> Result<NetworkResponse> {
886    // Validate request count (from config)
887    if getdata.inventory.len() > config.network_limits.max_inv_items {
888        return Ok(NetworkResponse::Reject(format!(
889            "Too many getdata items (max {})",
890            config.network_limits.max_inv_items
891        )));
892    }
893
894    // Send requested objects (if chain access provided)
895    if let Some(chain) = chain_access {
896        let mut responses = Vec::with_capacity(getdata.inventory.len());
897        for item in &getdata.inventory {
898            if let Some(obj) = chain.get_object(&item.hash) {
899                match item.inv_type {
900                    1 => {
901                        // MSG_TX
902                        if let Some(tx) = obj.as_transaction() {
903                            responses.push(NetworkMessage::Tx(Arc::clone(tx), Vec::new()));
904                        }
905                    }
906                    2 => {
907                        // MSG_BLOCK
908                        if let Some(block) = obj.as_block() {
909                            let Some(witnesses) = chain.get_block_witnesses(&item.hash) else {
910                                // REV-P-08: do not synthesize empty witness stacks when storage
911                                // has no witness data (node layer serves notfound instead).
912                                continue;
913                            };
914                            responses.push(NetworkMessage::Block(Arc::clone(block), witnesses));
915                        }
916                    }
917                    _ => {
918                        // Unknown inventory type - skip
919                    }
920                }
921            }
922        }
923
924        if !responses.is_empty() {
925            return Ok(NetworkResponse::SendMessages(responses));
926        }
927    }
928
929    Ok(NetworkResponse::Ok)
930}
931
932/// Process getheaders message
933fn process_getheaders_message(
934    getheaders: &GetHeadersMessage,
935    chain_access: Option<&dyn ChainStateAccess>,
936    config: &ProtocolConfig,
937) -> Result<NetworkResponse> {
938    // Validate block locator size (from config)
939    if getheaders.block_locator_hashes.len() > config.validation.max_locator_hashes {
940        return Ok(NetworkResponse::Reject(format!(
941            "Too many locator hashes (max {})",
942            config.validation.max_locator_hashes
943        )));
944    }
945
946    // Use chain access to find headers (if provided)
947    if let Some(chain) = chain_access {
948        let headers =
949            chain.get_headers_for_locator(&getheaders.block_locator_hashes, &getheaders.hash_stop);
950        return Ok(NetworkResponse::SendMessage(Box::new(
951            NetworkMessage::Headers(HeadersMessage { headers }),
952        )));
953    }
954
955    Ok(NetworkResponse::Reject("Chain access not available".into()))
956}
957
958/// Process headers message
959fn process_headers_message(
960    headers: &HeadersMessage,
961    config: &ProtocolConfig,
962) -> Result<NetworkResponse> {
963    // Validate header count (from config)
964    if headers.headers.len() > config.network_limits.max_headers {
965        return Ok(NetworkResponse::Reject(format!(
966            "Too many headers (max {})",
967            config.network_limits.max_headers
968        )));
969    }
970
971    // Header validation is consensus logic, not protocol
972    // Node layer will validate headers using consensus layer
973    Ok(NetworkResponse::Ok)
974}
975
976/// Process block message
977fn process_block_message(
978    engine: &BitcoinProtocolEngine,
979    block: &Block,
980    witnesses: &[Vec<blvm_consensus::segwit::Witness>],
981    utxo_set: Option<&UtxoSet>,
982    height: Option<u64>,
983    config: &ProtocolConfig,
984) -> Result<NetworkResponse> {
985    // Check protocol limits first (from config)
986    if block.transactions.len() > config.validation.max_txs_per_block {
987        return Err(crate::error::ProtocolError::MessageTooLarge {
988            size: block.transactions.len(),
989            max: config.validation.max_txs_per_block,
990        });
991    }
992
993    // Delegate to consensus via protocol engine (requires utxo_set and height)
994    if let (Some(utxos), Some(h)) = (utxo_set, height) {
995        let context = ProtocolValidationContext::new(engine.get_protocol_version(), h)?;
996        // Pass witnesses for full BIP141/Taproot weight and script validation.
997        let result = engine
998            .validate_block_with_protocol_and_witnesses(block, witnesses, utxos, h, &context)?;
999
1000        match result {
1001            ValidationResult::Valid => Ok(NetworkResponse::Ok),
1002            ValidationResult::Invalid(reason) => {
1003                Ok(NetworkResponse::Reject(format!("Invalid block: {reason}")))
1004            }
1005        }
1006    } else {
1007        Err(crate::error::ProtocolError::Configuration(
1008            "Missing validation context (utxo_set and height required)".into(),
1009        ))
1010    }
1011}
1012
1013/// Process transaction message
1014fn process_tx_message(
1015    engine: &BitcoinProtocolEngine,
1016    tx: &Transaction,
1017    _witnesses: &[crate::segwit::Witness],
1018    height: Option<u64>,
1019) -> Result<NetworkResponse> {
1020    // Check protocol limits and validate
1021    let context =
1022        ProtocolValidationContext::new(engine.get_protocol_version(), height.unwrap_or(0))?;
1023    let result = engine.validate_transaction_with_protocol(tx, &context)?;
1024
1025    match result {
1026        ValidationResult::Valid => Ok(NetworkResponse::Ok),
1027        ValidationResult::Invalid(reason) => Ok(NetworkResponse::Reject(format!(
1028            "Invalid transaction: {reason}"
1029        ))),
1030    }
1031}
1032
1033/// Process ping message
1034fn process_ping_message(
1035    ping: &PingMessage,
1036    _peer_state: &mut PeerState,
1037) -> Result<NetworkResponse> {
1038    let pong = NetworkMessage::Pong(PongMessage { nonce: ping.nonce });
1039    Ok(NetworkResponse::SendMessage(Box::new(pong)))
1040}
1041
1042/// Process pong message
1043fn process_pong_message(pong: &PongMessage, peer_state: &mut PeerState) -> Result<NetworkResponse> {
1044    // Validate pong nonce matches our ping
1045    if peer_state.ping_nonce == Some(pong.nonce) {
1046        peer_state.ping_nonce = None;
1047        peer_state.last_pong = Some(std::time::SystemTime::now());
1048    }
1049
1050    Ok(NetworkResponse::Ok)
1051}
1052
1053/// Process mempool message
1054fn process_mempool_message(chain_access: Option<&dyn ChainStateAccess>) -> Result<NetworkResponse> {
1055    // Send all mempool transactions (if chain access provided)
1056    if let Some(chain) = chain_access {
1057        let mempool_txs = chain.get_mempool_transactions();
1058        let mut responses = Vec::with_capacity(mempool_txs.len());
1059
1060        for tx in mempool_txs {
1061            responses.push(NetworkMessage::Tx(Arc::new(tx), Vec::new()));
1062        }
1063
1064        if !responses.is_empty() {
1065            return Ok(NetworkResponse::SendMessages(responses));
1066        }
1067    }
1068
1069    Ok(NetworkResponse::Ok)
1070}
1071
1072/// Process feefilter message
1073fn process_feefilter_message(
1074    feefilter: &FeeFilterMessage,
1075    peer_state: &mut PeerState,
1076) -> Result<NetworkResponse> {
1077    peer_state.min_fee_rate = Some(feefilter.feerate);
1078    Ok(NetworkResponse::Ok)
1079}
1080
1081/// Process getblocks message
1082fn process_getblocks_message(
1083    getblocks: &GetBlocksMessage,
1084    chain_access: Option<&dyn ChainStateAccess>,
1085    config: &ProtocolConfig,
1086) -> Result<NetworkResponse> {
1087    // Validate block locator size (from config)
1088    if getblocks.block_locator_hashes.len() > config.validation.max_locator_hashes {
1089        return Ok(NetworkResponse::Reject(format!(
1090            "Too many locator hashes (max {})",
1091            config.validation.max_locator_hashes
1092        )));
1093    }
1094
1095    if let Some(chain) = chain_access {
1096        use blvm_consensus::block::block_header_hash;
1097
1098        // Core-compatible: walk locator to fork, then advertise block hashes after fork.
1099        const MAX_GETBLOCKS_INV: usize = 500;
1100        let headers =
1101            chain.get_headers_for_locator(&getblocks.block_locator_hashes, &getblocks.hash_stop);
1102        if headers.is_empty() {
1103            return Ok(NetworkResponse::Ok);
1104        }
1105
1106        let inventory: Vec<InventoryVector> = headers
1107            .into_iter()
1108            .take(MAX_GETBLOCKS_INV)
1109            .map(|header| InventoryVector {
1110                inv_type: 2, // MSG_BLOCK
1111                hash: block_header_hash(&header),
1112            })
1113            .collect();
1114
1115        return Ok(NetworkResponse::SendMessage(Box::new(NetworkMessage::Inv(
1116            InvMessage { inventory },
1117        ))));
1118    }
1119
1120    Ok(NetworkResponse::Ok)
1121}
1122
1123/// Process getaddr message
1124fn process_getaddr_message(
1125    peer_state: &mut PeerState,
1126    config: &ProtocolConfig,
1127) -> Result<NetworkResponse> {
1128    // Return known addresses (if any)
1129    if !peer_state.known_addresses.is_empty() {
1130        // Limit to configured max addresses
1131        let max_addrs = config
1132            .network_limits
1133            .max_addr_addresses
1134            .min(peer_state.known_addresses.len());
1135        let mut addresses = Vec::with_capacity(max_addrs);
1136        addresses.extend(peer_state.known_addresses.iter().take(max_addrs).cloned());
1137
1138        return Ok(NetworkResponse::SendMessage(Box::new(
1139            NetworkMessage::Addr(AddrMessage { addresses }),
1140        )));
1141    }
1142
1143    Ok(NetworkResponse::Ok)
1144}
1145
1146/// Process notfound message
1147fn process_notfound_message(
1148    notfound: &NotFoundMessage,
1149    config: &ProtocolConfig,
1150) -> Result<NetworkResponse> {
1151    // Validate inventory count (from config)
1152    if notfound.inventory.len() > config.network_limits.max_inv_items {
1153        return Ok(NetworkResponse::Reject(format!(
1154            "Too many notfound items (max {})",
1155            config.network_limits.max_inv_items
1156        )));
1157    }
1158
1159    // NotFound is informational - just acknowledge
1160    Ok(NetworkResponse::Ok)
1161}
1162
1163/// Process reject message
1164fn process_reject_message(
1165    reject: &RejectMessage,
1166    _config: &ProtocolConfig,
1167) -> Result<NetworkResponse> {
1168    // Validate message name length (Bitcoin protocol limit: 12 bytes)
1169    // This is a fixed protocol limit, not configurable
1170    if reject.message.len() > 12 {
1171        return Ok(NetworkResponse::Reject(
1172            "Invalid reject message name".into(),
1173        ));
1174    }
1175
1176    // Validate reason length (Bitcoin protocol limit: 111 bytes)
1177    // This is a fixed protocol limit, not configurable
1178    if reject.reason.len() > 111 {
1179        return Ok(NetworkResponse::Reject("Reject reason too long".into()));
1180    }
1181
1182    // Reject is informational - log and acknowledge
1183    // In production, this would trigger appropriate handling (ban peer, etc.)
1184    Ok(NetworkResponse::Ok)
1185}
1186
1187/// Process sendheaders message
1188fn process_sendheaders_message(peer_state: &mut PeerState) -> Result<NetworkResponse> {
1189    peer_state.prefer_headers = true;
1190    Ok(NetworkResponse::Ok)
1191}
1192
1193/// Process sendcmpct message (BIP152)
1194fn process_sendcmpct_message(
1195    sendcmpct: &SendCmpctMessage,
1196    peer_state: &mut PeerState,
1197    config: &ProtocolConfig,
1198) -> Result<NetworkResponse> {
1199    // Validate version (must be 1 or 2, or match configured preferred version)
1200    let valid_versions = [1, 2];
1201    if !valid_versions.contains(&sendcmpct.version) {
1202        return Ok(NetworkResponse::Reject(
1203            "Invalid compact block version".into(),
1204        ));
1205    }
1206
1207    // Check if compact blocks are enabled
1208    if !config.compact_blocks.enabled {
1209        return Ok(NetworkResponse::Reject("Compact blocks not enabled".into()));
1210    }
1211
1212    // Store compact block preference in peer state
1213    peer_state.compact_block_version = Some(sendcmpct.version as u8);
1214    peer_state.prefer_compact_block = sendcmpct.prefer_cmpct != 0;
1215    Ok(NetworkResponse::Ok)
1216}
1217
1218fn txid_of(tx: &Transaction) -> Hash {
1219    use sha2::{Digest, Sha256};
1220    let bytes = crate::wire::serialize_tx(tx).unwrap_or_default();
1221    let first = Sha256::digest(&bytes);
1222    let second = Sha256::digest(first);
1223    let mut hash = [0u8; 32];
1224    hash.copy_from_slice(&second);
1225    hash
1226}
1227
1228fn header_hash(header: &BlockHeader) -> Hash {
1229    use sha2::{Digest, Sha256};
1230    let bytes = crate::serialization::serialize_block_header(header);
1231    let first = Sha256::digest(&bytes);
1232    let second = Sha256::digest(first);
1233    let mut hash = [0u8; 32];
1234    hash.copy_from_slice(&second);
1235    hash
1236}
1237
1238fn short_id_hex(id: &ShortTxId) -> String {
1239    hex::encode(id)
1240}
1241
1242/// Place prefilled transactions and unique mempool matches. Missing short ids stay empty.
1243fn begin_pending_compact(
1244    msg: &CmpctBlockMessage,
1245    mempool: &[Transaction],
1246) -> std::result::Result<PendingCompact, String> {
1247    let n = msg.short_ids.len() + msg.prefilled_txs.len();
1248    let mut slots: Vec<Option<(Transaction, Vec<blvm_consensus::segwit::Witness>)>> = vec![None; n];
1249    let mut prefilled = std::collections::HashSet::new();
1250    for item in &msg.prefilled_txs {
1251        let index = usize::from(item.index);
1252        if index >= n || !prefilled.insert(index) {
1253            return Err(format!(
1254                "prefilled compact-block index {index} is not usable"
1255            ));
1256        }
1257        slots[index] = Some((item.tx.clone(), item.witness.clone().unwrap_or_default()));
1258    }
1259
1260    let mut missing = Vec::new();
1261    let mut next = 0usize;
1262    for short_id in &msg.short_ids {
1263        while prefilled.contains(&next) {
1264            next = next.saturating_add(1);
1265        }
1266        let block_index = next;
1267        next = next.saturating_add(1);
1268        if block_index >= n {
1269            return Err("compact block short ids ran past the transaction list".into());
1270        }
1271        let mut found: Option<(Transaction, Vec<blvm_consensus::segwit::Witness>)> = None;
1272        let mut hits = 0usize;
1273        for tx in mempool {
1274            let id = crate::bip152::short_tx_id(&msg.header, &txid_of(tx), msg.nonce);
1275            if &id == short_id {
1276                hits += 1;
1277                found = Some((tx.clone(), Vec::new()));
1278            }
1279        }
1280        if hits == 1 {
1281            slots[block_index] = found;
1282        } else {
1283            missing.push((block_index, *short_id));
1284        }
1285    }
1286    Ok(PendingCompact {
1287        header: msg.header.clone(),
1288        nonce: msg.nonce,
1289        slots,
1290        missing,
1291    })
1292}
1293
1294fn finish_compact_block(
1295    engine: &BitcoinProtocolEngine,
1296    pending: PendingCompact,
1297    utxo_set: Option<&UtxoSet>,
1298    height: Option<u64>,
1299    config: &ProtocolConfig,
1300) -> Result<NetworkResponse> {
1301    if !pending.missing.is_empty() {
1302        let id = short_id_hex(&pending.missing[0].1);
1303        return Ok(NetworkResponse::Reject(format!(
1304            "short id {id} matches neither the mempool nor blocktxn"
1305        )));
1306    }
1307    let mut transactions = Vec::with_capacity(pending.slots.len());
1308    let mut witnesses = Vec::with_capacity(pending.slots.len());
1309    for slot in pending.slots {
1310        let Some((tx, witness)) = slot else {
1311            return Ok(NetworkResponse::Reject(
1312                "compact block still has an empty transaction slot".into(),
1313            ));
1314        };
1315        transactions.push(tx);
1316        witnesses.push(witness);
1317    }
1318    let block = Block::from_parts(pending.header, transactions.into_boxed_slice());
1319    match process_block_message(engine, &block, &witnesses, utxo_set, height, config) {
1320        Ok(response) => Ok(response),
1321        Err(err) if err.to_string().contains("Missing validation context") => {
1322            Ok(NetworkResponse::SendMessage(Box::new(
1323                NetworkMessage::Block(Arc::new(block), witnesses),
1324            )))
1325        }
1326        Err(err) => Err(err),
1327    }
1328}
1329
1330/// Process cmpctblock message (BIP152)
1331fn process_cmpctblock_message(
1332    engine: &BitcoinProtocolEngine,
1333    cmpctblock: &CmpctBlockMessage,
1334    peer_state: &mut PeerState,
1335    chain_access: Option<&dyn ChainStateAccess>,
1336    utxo_set: Option<&UtxoSet>,
1337    height: Option<u64>,
1338    config: &ProtocolConfig,
1339) -> Result<NetworkResponse> {
1340    if !config.compact_blocks.enabled {
1341        return Ok(NetworkResponse::Reject("Compact blocks not enabled".into()));
1342    }
1343    let mempool = chain_access
1344        .map(|chain| chain.get_mempool_transactions())
1345        .unwrap_or_default();
1346    let pending = match begin_pending_compact(cmpctblock, &mempool) {
1347        Ok(pending) => pending,
1348        Err(reason) => return Ok(NetworkResponse::Reject(reason)),
1349    };
1350    if pending.missing.is_empty() {
1351        peer_state.pending_compact = None;
1352        return finish_compact_block(engine, pending, utxo_set, height, config);
1353    }
1354    let indices: Vec<u16> = pending
1355        .missing
1356        .iter()
1357        .map(|(index, _)| u16::try_from(*index).unwrap_or(u16::MAX))
1358        .collect();
1359    let block_hash = header_hash(&pending.header);
1360    peer_state.pending_compact = Some(pending);
1361    Ok(NetworkResponse::SendMessage(Box::new(
1362        NetworkMessage::GetBlockTxn(GetBlockTxnMessage {
1363            block_hash,
1364            indices,
1365        }),
1366    )))
1367}
1368
1369/// Process getblocktxn message (BIP152)
1370fn process_getblocktxn_message(
1371    getblocktxn: &GetBlockTxnMessage,
1372    chain_access: Option<&dyn ChainStateAccess>,
1373    config: &ProtocolConfig,
1374) -> Result<NetworkResponse> {
1375    // Validate indices count (from config)
1376    if getblocktxn.indices.len() > config.compact_blocks.max_blocktxn_indices {
1377        return Ok(NetworkResponse::Reject(format!(
1378            "Too many transaction indices (max {})",
1379            config.compact_blocks.max_blocktxn_indices
1380        )));
1381    }
1382
1383    // Use chain access to get requested transactions and witnesses
1384    if let Some(chain) = chain_access {
1385        if let Some(obj) = chain.get_object(&getblocktxn.block_hash) {
1386            if let Some(block) = obj.as_block() {
1387                let stored_witnesses = chain.get_block_witnesses(&getblocktxn.block_hash);
1388                let mut transactions = Vec::with_capacity(getblocktxn.indices.len());
1389                let mut req_witnesses = Vec::with_capacity(getblocktxn.indices.len());
1390
1391                for &index in &getblocktxn.indices {
1392                    let idx = index as usize;
1393                    if idx >= block.transactions.len() {
1394                        return Ok(NetworkResponse::Reject(
1395                            "getblocktxn index past end of block".into(),
1396                        ));
1397                    }
1398                    transactions.push(block.transactions[idx].clone());
1399                    let tx_witness = stored_witnesses
1400                        .as_ref()
1401                        .and_then(|w| w.get(idx).cloned())
1402                        .unwrap_or_else(|| {
1403                            block.transactions[idx]
1404                                .inputs
1405                                .iter()
1406                                .map(|_| Vec::new())
1407                                .collect()
1408                        });
1409                    req_witnesses.push(tx_witness);
1410                }
1411
1412                if !transactions.is_empty() {
1413                    return Ok(NetworkResponse::SendMessage(Box::new(
1414                        NetworkMessage::BlockTxn(BlockTxnMessage {
1415                            block_hash: getblocktxn.block_hash,
1416                            transactions,
1417                            witnesses: Some(req_witnesses),
1418                        }),
1419                    )));
1420                }
1421            }
1422        }
1423    }
1424
1425    Ok(NetworkResponse::Ok)
1426}
1427
1428/// Process blocktxn message (BIP152)
1429fn process_blocktxn_message(
1430    engine: &BitcoinProtocolEngine,
1431    blocktxn: &BlockTxnMessage,
1432    peer_state: &mut PeerState,
1433    utxo_set: Option<&UtxoSet>,
1434    height: Option<u64>,
1435    config: &ProtocolConfig,
1436) -> Result<NetworkResponse> {
1437    let Some(mut pending) = peer_state.pending_compact.clone() else {
1438        return Ok(NetworkResponse::Reject(
1439            "blocktxn does not match a pending compact block".into(),
1440        ));
1441    };
1442    if header_hash(&pending.header) != blocktxn.block_hash {
1443        return Ok(NetworkResponse::Reject(
1444            "blocktxn does not match a pending compact block".into(),
1445        ));
1446    }
1447    if blocktxn.transactions.len() != pending.missing.len() {
1448        let id = pending
1449            .missing
1450            .first()
1451            .map(|(_, short_id)| short_id_hex(short_id))
1452            .unwrap_or_else(|| "none".into());
1453        return Ok(NetworkResponse::Reject(format!(
1454            "short id {id} matches neither the mempool nor blocktxn"
1455        )));
1456    }
1457    for (i, (index, expected)) in pending.missing.iter().enumerate() {
1458        let tx = &blocktxn.transactions[i];
1459        let got = crate::bip152::short_tx_id(&pending.header, &txid_of(tx), pending.nonce);
1460        if &got != expected {
1461            return Ok(NetworkResponse::Reject(format!(
1462                "short id {} matches neither the mempool nor blocktxn",
1463                short_id_hex(expected)
1464            )));
1465        }
1466        let witness = blocktxn
1467            .witnesses
1468            .as_ref()
1469            .and_then(|all| all.get(i).cloned())
1470            .unwrap_or_default();
1471        pending.slots[*index] = Some((tx.clone(), witness));
1472    }
1473    pending.missing.clear();
1474    peer_state.pending_compact = None;
1475    finish_compact_block(engine, pending, utxo_set, height, config)
1476}
1477
1478#[cfg(feature = "utxo-commitments")]
1479/// Process getutxoset message
1480fn process_getutxoset_message(
1481    getutxoset: &commons::GetUTXOSetMessage,
1482    chain_access: Option<&dyn ChainStateAccess>,
1483) -> Result<NetworkResponse> {
1484    let Some(chain) = chain_access else {
1485        return Err(ProtocolError::Validation(
1486            "utxo set is not available".into(),
1487        ));
1488    };
1489    match chain.stored_utxo_commitment(&getutxoset.block_hash) {
1490        Ok(Some(commitment)) => Ok(NetworkResponse::SendMessage(Box::new(
1491            NetworkMessage::UTXOSet(commons::UTXOSetMessage {
1492                request_id: 0,
1493                utxo_count: commitment.utxo_count,
1494                is_complete: true,
1495                chunk_id: None,
1496                commitment,
1497            }),
1498        ))),
1499        Ok(None) => Err(ProtocolError::Validation(
1500            format!(
1501                "no stored UTXO set for block {}",
1502                hex::encode(getutxoset.block_hash)
1503            )
1504            .into(),
1505        )),
1506        Err(reason) => Err(ProtocolError::Validation(reason.into())),
1507    }
1508}
1509
1510#[cfg(feature = "utxo-commitments")]
1511/// Process utxoset message
1512fn process_utxoset_message(
1513    utxoset: &commons::UTXOSetMessage,
1514    chain_access: Option<&dyn ChainStateAccess>,
1515) -> Result<NetworkResponse> {
1516    let Some(chain) = chain_access else {
1517        return Ok(NetworkResponse::Reject(
1518            "utxo commitment does not match the local set".into(),
1519        ));
1520    };
1521    match chain.stored_utxo_commitment(&utxoset.commitment.block_hash) {
1522        Ok(Some(local)) if local == utxoset.commitment => Ok(NetworkResponse::Ok),
1523        Ok(_) => Ok(NetworkResponse::Reject(format!(
1524            "utxo commitment does not match the local set for {}",
1525            hex::encode(utxoset.commitment.block_hash)
1526        ))),
1527        Err(reason) => Ok(NetworkResponse::Reject(reason)),
1528    }
1529}
1530
1531#[cfg(feature = "utxo-commitments")]
1532/// Process getfilteredblock message
1533fn process_getfilteredblock_message(
1534    getfiltered: &commons::GetFilteredBlockMessage,
1535    chain_access: Option<&dyn ChainStateAccess>,
1536) -> Result<NetworkResponse> {
1537    let Some(chain) = chain_access else {
1538        return Err(ProtocolError::Validation(
1539            "block filter is not available".into(),
1540        ));
1541    };
1542    match chain.stored_block_filter(&getfiltered.block_hash) {
1543        Ok(Some((header, filter))) => {
1544            let commitment = chain
1545                .stored_utxo_commitment(&getfiltered.block_hash)
1546                .ok()
1547                .flatten()
1548                .unwrap_or(commons::UTXOCommitment {
1549                    merkle_root: [0u8; 32],
1550                    total_supply: 0,
1551                    utxo_count: 0,
1552                    block_height: 0,
1553                    block_hash: getfiltered.block_hash,
1554                });
1555            Ok(NetworkResponse::SendMessage(Box::new(
1556                NetworkMessage::FilteredBlock(commons::FilteredBlockMessage {
1557                    request_id: getfiltered.request_id,
1558                    header,
1559                    commitment,
1560                    transactions: Vec::new(),
1561                    transaction_indices: Vec::new(),
1562                    spam_summary: commons::SpamSummary {
1563                        filtered_count: 0,
1564                        filtered_value: 0,
1565                        filter_reasons: 0,
1566                    },
1567                    bip158_filter: Some(filter),
1568                }),
1569            )))
1570        }
1571        Ok(None) => Err(ProtocolError::Validation(
1572            format!(
1573                "no block filter for {}",
1574                hex::encode(getfiltered.block_hash)
1575            )
1576            .into(),
1577        )),
1578        Err(reason) => Err(ProtocolError::Validation(reason.into())),
1579    }
1580}
1581
1582#[cfg(feature = "utxo-commitments")]
1583/// Process filteredblock message
1584fn process_filteredblock_message(
1585    filtered: &commons::FilteredBlockMessage,
1586    chain_access: Option<&dyn ChainStateAccess>,
1587) -> Result<NetworkResponse> {
1588    let Some(chain) = chain_access else {
1589        return Ok(NetworkResponse::Reject(
1590            "filtered block does not match the local filter".into(),
1591        ));
1592    };
1593    let block_hash = header_hash(&filtered.header);
1594    match chain.stored_block_filter(&block_hash) {
1595        Ok(Some((_, local))) if filtered.bip158_filter.as_ref() == Some(&local) => {
1596            Ok(NetworkResponse::Ok)
1597        }
1598        Ok(_) => Ok(NetworkResponse::Reject(format!(
1599            "filtered block does not match the local filter for {}",
1600            hex::encode(block_hash)
1601        ))),
1602        Err(reason) => Ok(NetworkResponse::Reject(reason)),
1603    }
1604}
1605
1606/// Process getbanlist message
1607fn process_getbanlist_message(
1608    getbanlist: &commons::GetBanListMessage,
1609    chain_access: Option<&dyn ChainStateAccess>,
1610) -> Result<NetworkResponse> {
1611    let mut entries = chain_access
1612        .map(|chain| chain.ban_entries())
1613        .unwrap_or_default();
1614    if let Some(min_score) = getbanlist.min_score {
1615        entries.retain(|entry| entry.score >= min_score);
1616    }
1617    Ok(NetworkResponse::SendMessage(Box::new(
1618        NetworkMessage::BanList(commons::BanListMessage {
1619            request_id: getbanlist.request_id,
1620            entries,
1621            list_signature: None,
1622        }),
1623    )))
1624}
1625
1626/// Process banlist message
1627fn process_banlist_message(
1628    banlist: &commons::BanListMessage,
1629    chain_access: Option<&dyn ChainStateAccess>,
1630) -> Result<NetworkResponse> {
1631    if banlist.entries.is_empty() {
1632        return Ok(NetworkResponse::Ok);
1633    }
1634    let Some(chain) = chain_access else {
1635        return Ok(NetworkResponse::Reject("ban path is not available".into()));
1636    };
1637    for entry in &banlist.entries {
1638        if let Err(reason) = chain.apply_ban(entry) {
1639            return Ok(NetworkResponse::Reject(reason));
1640        }
1641    }
1642    Ok(NetworkResponse::Ok)
1643}
1644
1645#[cfg(test)]
1646fn assert_getblocktxn_past_end_rejects() {
1647    struct TwoTx;
1648    impl ChainStateAccess for TwoTx {
1649        fn has_object(&self, _hash: &Hash) -> bool {
1650            true
1651        }
1652        fn get_object(&self, hash: &Hash) -> Option<ChainObject> {
1653            let tx = Transaction {
1654                version: 1,
1655                inputs: vec![].into(),
1656                outputs: vec![].into(),
1657                lock_time: 0,
1658            };
1659            let block = Block {
1660                header: BlockHeader {
1661                    version: 1,
1662                    prev_block_hash: [0; 32],
1663                    merkle_root: [0; 32],
1664                    timestamp: 1,
1665                    bits: 1,
1666                    nonce: 1,
1667                },
1668                transactions: vec![tx.clone(), tx].into_boxed_slice(),
1669            };
1670            if hash == &[9u8; 32] {
1671                Some(ChainObject::Block(Arc::new(block)))
1672            } else {
1673                None
1674            }
1675        }
1676        fn get_headers_for_locator(&self, _locator: &[Hash], _stop: &Hash) -> Vec<BlockHeader> {
1677            Vec::new()
1678        }
1679        fn get_mempool_transactions(&self) -> Vec<Transaction> {
1680            Vec::new()
1681        }
1682    }
1683    let msg = GetBlockTxnMessage {
1684        block_hash: [9u8; 32],
1685        indices: vec![5],
1686    };
1687    let response =
1688        process_getblocktxn_message(&msg, Some(&TwoTx), &ProtocolConfig::default()).unwrap();
1689    assert!(
1690        matches!(response, NetworkResponse::Reject(_)),
1691        "expected reject, got {response:?}"
1692    );
1693}
1694
1695#[cfg(test)]
1696#[test]
1697fn getblocktxn_index_past_block_returns_reject_and_no_blocktxn() {
1698    assert_getblocktxn_past_end_rejects();
1699}
1700
1701#[cfg(test)]
1702fn sample_tx(tag: u8) -> Transaction {
1703    Transaction {
1704        version: 1,
1705        inputs: vec![blvm_consensus::types::TransactionInput {
1706            prevout: blvm_consensus::types::OutPoint {
1707                hash: [tag; 32],
1708                index: 0,
1709            },
1710            script_sig: vec![tag],
1711            sequence: 0xffffffff,
1712        }]
1713        .into(),
1714        outputs: vec![blvm_consensus::types::TransactionOutput {
1715            value: 1000,
1716            script_pubkey: vec![0x51],
1717        }]
1718        .into(),
1719        lock_time: 0,
1720    }
1721}
1722
1723#[cfg(test)]
1724fn sample_header() -> BlockHeader {
1725    BlockHeader {
1726        version: 1,
1727        prev_block_hash: [2u8; 32],
1728        merkle_root: [3u8; 32],
1729        timestamp: 1_700_000_000,
1730        bits: 0x1d00ffff,
1731        nonce: 4,
1732    }
1733}
1734
1735#[cfg(test)]
1736struct MempoolChain(Transaction);
1737
1738#[cfg(test)]
1739impl ChainStateAccess for MempoolChain {
1740    fn has_object(&self, _hash: &Hash) -> bool {
1741        false
1742    }
1743    fn get_object(&self, _hash: &Hash) -> Option<ChainObject> {
1744        None
1745    }
1746    fn get_headers_for_locator(&self, _locator: &[Hash], _stop: &Hash) -> Vec<BlockHeader> {
1747        Vec::new()
1748    }
1749    fn get_mempool_transactions(&self) -> Vec<Transaction> {
1750        vec![self.0.clone()]
1751    }
1752}
1753
1754#[cfg(test)]
1755#[test]
1756fn compact_block_rebuilds_from_mempool_and_names_a_miss() {
1757    use crate::{BitcoinProtocolEngine, ProtocolVersion};
1758    let engine = BitcoinProtocolEngine::new(ProtocolVersion::BitcoinV1).unwrap();
1759    let header = sample_header();
1760    let coinbase = sample_tx(0);
1761    let pooled = sample_tx(7);
1762    let nonce = 9u64;
1763    let short = crate::bip152::short_tx_id(&header, &txid_of(&pooled), nonce);
1764    let cmpct = CmpctBlockMessage {
1765        header: header.clone(),
1766        nonce,
1767        short_ids: vec![short],
1768        prefilled_txs: vec![PrefilledTransaction {
1769            index: 0,
1770            tx: coinbase.clone(),
1771            witness: None,
1772        }],
1773    };
1774    let mut peer = PeerState::new();
1775    let response = process_cmpctblock_message(
1776        &engine,
1777        &cmpct,
1778        &mut peer,
1779        Some(&MempoolChain(pooled.clone())),
1780        None,
1781        None,
1782        &ProtocolConfig::default(),
1783    )
1784    .unwrap();
1785    match response {
1786        NetworkResponse::SendMessage(message) => match *message {
1787            NetworkMessage::Block(block, _) => {
1788                assert_eq!(block.header, header);
1789                assert_eq!(block.transactions.len(), 2);
1790                assert_eq!(block.transactions[0], coinbase);
1791                assert_eq!(block.transactions[1], pooled);
1792            }
1793            other => panic!("expected the reconstructed block, got {other:?}"),
1794        },
1795        other => panic!("expected the reconstructed block, got {other:?}"),
1796    }
1797
1798    let missing = [0xabu8; 6];
1799    let cmpct = CmpctBlockMessage {
1800        header: header.clone(),
1801        nonce,
1802        short_ids: vec![missing],
1803        prefilled_txs: vec![PrefilledTransaction {
1804            index: 0,
1805            tx: coinbase,
1806            witness: None,
1807        }],
1808    };
1809    let mut peer = PeerState::new();
1810    let asked = process_cmpctblock_message(
1811        &engine,
1812        &cmpct,
1813        &mut peer,
1814        Some(&MempoolChain(pooled.clone())),
1815        None,
1816        None,
1817        &ProtocolConfig::default(),
1818    )
1819    .unwrap();
1820    assert!(matches!(
1821        asked,
1822        NetworkResponse::SendMessage(ref message) if matches!(**message, NetworkMessage::GetBlockTxn(_))
1823    ));
1824    let wrong = BlockTxnMessage {
1825        block_hash: header_hash(&header),
1826        transactions: vec![pooled],
1827        witnesses: None,
1828    };
1829    let rejected = process_blocktxn_message(
1830        &engine,
1831        &wrong,
1832        &mut peer,
1833        None,
1834        None,
1835        &ProtocolConfig::default(),
1836    )
1837    .unwrap();
1838    match rejected {
1839        NetworkResponse::Reject(reason) => {
1840            assert!(reason.contains(&short_id_hex(&missing)), "{reason}");
1841        }
1842        other => panic!("expected a named short-id reject, got {other:?}"),
1843    }
1844}
1845
1846#[cfg(test)]
1847struct BanBook {
1848    entries: std::sync::Mutex<Vec<crate::commons::BanEntry>>,
1849}
1850
1851#[cfg(test)]
1852impl ChainStateAccess for BanBook {
1853    fn has_object(&self, _hash: &Hash) -> bool {
1854        false
1855    }
1856    fn get_object(&self, _hash: &Hash) -> Option<ChainObject> {
1857        None
1858    }
1859    fn get_headers_for_locator(&self, _locator: &[Hash], _stop: &Hash) -> Vec<BlockHeader> {
1860        Vec::new()
1861    }
1862    fn get_mempool_transactions(&self) -> Vec<Transaction> {
1863        Vec::new()
1864    }
1865    fn ban_entries(&self) -> Vec<crate::commons::BanEntry> {
1866        self.entries.lock().expect("ban book").clone()
1867    }
1868    fn apply_ban(&self, entry: &crate::commons::BanEntry) -> std::result::Result<(), String> {
1869        self.entries.lock().expect("ban book").push(entry.clone());
1870        Ok(())
1871    }
1872}
1873
1874#[cfg(test)]
1875#[test]
1876fn ban_messages_list_and_apply_without_the_old_reject() {
1877    let book = BanBook {
1878        entries: std::sync::Mutex::new(Vec::new()),
1879    };
1880    let empty = process_getbanlist_message(
1881        &crate::commons::GetBanListMessage {
1882            request_id: 1,
1883            min_score: None,
1884        },
1885        Some(&book),
1886    )
1887    .unwrap();
1888    match empty {
1889        NetworkResponse::SendMessage(message) => match *message {
1890            NetworkMessage::BanList(list) => assert!(list.entries.is_empty()),
1891            other => panic!("expected an empty ban list, got {other:?}"),
1892        },
1893        other => panic!("expected an empty ban list, got {other:?}"),
1894    }
1895    let entry = crate::commons::BanEntry {
1896        ip: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 1, 2, 3, 4],
1897        score: 100,
1898        reason: 1,
1899        timestamp: 1_700_000_000,
1900        signature: None,
1901    };
1902    process_banlist_message(
1903        &crate::commons::BanListMessage {
1904            request_id: 2,
1905            entries: vec![entry.clone()],
1906            list_signature: None,
1907        },
1908        Some(&book),
1909    )
1910    .unwrap();
1911    assert_eq!(book.entries.lock().unwrap().as_slice(), &[entry]);
1912    let none = process_getbanlist_message(
1913        &crate::commons::GetBanListMessage {
1914            request_id: 3,
1915            min_score: None,
1916        },
1917        None,
1918    )
1919    .unwrap();
1920    assert!(matches!(none, NetworkResponse::SendMessage(_)));
1921}
1922
1923#[cfg(all(test, feature = "utxo-commitments"))]
1924struct UtxoBook {
1925    hash: Hash,
1926    commitment: crate::commons::UTXOCommitment,
1927    header: BlockHeader,
1928    filter: Vec<u8>,
1929}
1930
1931#[cfg(all(test, feature = "utxo-commitments"))]
1932impl ChainStateAccess for UtxoBook {
1933    fn has_object(&self, _hash: &Hash) -> bool {
1934        false
1935    }
1936    fn get_object(&self, _hash: &Hash) -> Option<ChainObject> {
1937        None
1938    }
1939    fn get_headers_for_locator(&self, _locator: &[Hash], _stop: &Hash) -> Vec<BlockHeader> {
1940        Vec::new()
1941    }
1942    fn get_mempool_transactions(&self) -> Vec<Transaction> {
1943        Vec::new()
1944    }
1945    fn stored_utxo_commitment(
1946        &self,
1947        block_hash: &Hash,
1948    ) -> std::result::Result<Option<crate::commons::UTXOCommitment>, String> {
1949        if block_hash == &self.hash {
1950            Ok(Some(self.commitment.clone()))
1951        } else {
1952            Ok(None)
1953        }
1954    }
1955    fn stored_block_filter(
1956        &self,
1957        block_hash: &Hash,
1958    ) -> std::result::Result<Option<(BlockHeader, Vec<u8>)>, String> {
1959        if block_hash == &self.hash {
1960            Ok(Some((self.header.clone(), self.filter.clone())))
1961        } else {
1962            Ok(None)
1963        }
1964    }
1965}
1966
1967#[cfg(all(test, feature = "utxo-commitments"))]
1968#[test]
1969fn utxo_and_filter_messages_use_the_stored_set() {
1970    let hash = [9u8; 32];
1971    let commitment = crate::commons::UTXOCommitment {
1972        merkle_root: [4u8; 32],
1973        total_supply: 50,
1974        utxo_count: 2,
1975        block_height: 10,
1976        block_hash: hash,
1977    };
1978    let book = UtxoBook {
1979        hash,
1980        commitment: commitment.clone(),
1981        header: sample_header(),
1982        filter: vec![0x01, 0x02, 0x03],
1983    };
1984    let got = process_getutxoset_message(
1985        &crate::commons::GetUTXOSetMessage {
1986            height: 10,
1987            block_hash: hash,
1988        },
1989        Some(&book),
1990    )
1991    .unwrap();
1992    match got {
1993        NetworkResponse::SendMessage(message) => match *message {
1994            NetworkMessage::UTXOSet(msg) => assert_eq!(msg.commitment, commitment),
1995            other => panic!("expected the stored commitment, got {other:?}"),
1996        },
1997        other => panic!("expected the stored commitment, got {other:?}"),
1998    }
1999    let missing = process_getutxoset_message(
2000        &crate::commons::GetUTXOSetMessage {
2001            height: 11,
2002            block_hash: [1u8; 32],
2003        },
2004        Some(&book),
2005    );
2006    let err = missing.expect_err("a missing set is an error");
2007    assert!(!err.to_string().contains("not implemented"));
2008
2009    let matched = process_utxoset_message(
2010        &crate::commons::UTXOSetMessage {
2011            request_id: 1,
2012            commitment: commitment.clone(),
2013            utxo_count: 2,
2014            is_complete: true,
2015            chunk_id: None,
2016        },
2017        Some(&book),
2018    )
2019    .unwrap();
2020    assert!(matches!(matched, NetworkResponse::Ok));
2021    let mut other_commitment = commitment.clone();
2022    other_commitment.total_supply = 1;
2023    let rejected = process_utxoset_message(
2024        &crate::commons::UTXOSetMessage {
2025            request_id: 1,
2026            commitment: other_commitment,
2027            utxo_count: 2,
2028            is_complete: true,
2029            chunk_id: None,
2030        },
2031        Some(&book),
2032    )
2033    .unwrap();
2034    assert!(matches!(rejected, NetworkResponse::Reject(_)));
2035
2036    let filter_msg = process_getfilteredblock_message(
2037        &crate::commons::GetFilteredBlockMessage {
2038            request_id: 7,
2039            block_hash: hash,
2040            filter_preferences: crate::commons::FilterPreferences {
2041                filter_ordinals: false,
2042                filter_dust: false,
2043                filter_brc20: false,
2044                min_output_value: 0,
2045            },
2046            include_bip158_filter: true,
2047        },
2048        Some(&book),
2049    )
2050    .unwrap();
2051    let sent_filter = match filter_msg {
2052        NetworkResponse::SendMessage(message) => match *message {
2053            NetworkMessage::FilteredBlock(msg) => msg.bip158_filter.expect("filter bytes"),
2054            other => panic!("expected a filter, got {other:?}"),
2055        },
2056        other => panic!("expected a filter, got {other:?}"),
2057    };
2058    assert_eq!(sent_filter, vec![0x01, 0x02, 0x03]);
2059
2060    let header = sample_header();
2061    let stored_hash = header_hash(&header);
2062    let book = UtxoBook {
2063        hash: stored_hash,
2064        commitment,
2065        header: header.clone(),
2066        filter: vec![0x01, 0x02, 0x03],
2067    };
2068    let ok = process_filteredblock_message(
2069        &crate::commons::FilteredBlockMessage {
2070            request_id: 7,
2071            header: header.clone(),
2072            commitment: book.commitment.clone(),
2073            transactions: Vec::new(),
2074            transaction_indices: Vec::new(),
2075            spam_summary: crate::commons::SpamSummary {
2076                filtered_count: 0,
2077                filtered_value: 0,
2078                filter_reasons: 0,
2079            },
2080            bip158_filter: Some(vec![0x01, 0x02, 0x03]),
2081        },
2082        Some(&book),
2083    )
2084    .unwrap();
2085    assert!(matches!(ok, NetworkResponse::Ok));
2086    let bad = process_filteredblock_message(
2087        &crate::commons::FilteredBlockMessage {
2088            request_id: 7,
2089            header,
2090            commitment: book.commitment.clone(),
2091            transactions: Vec::new(),
2092            transaction_indices: Vec::new(),
2093            spam_summary: crate::commons::SpamSummary {
2094                filtered_count: 0,
2095                filtered_value: 0,
2096                filter_reasons: 0,
2097            },
2098            bip158_filter: Some(vec![0xff]),
2099        },
2100        Some(&book),
2101    )
2102    .unwrap();
2103    assert!(matches!(bad, NetworkResponse::Reject(_)));
2104}