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