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/// Peer connection state
468#[derive(Clone)]
469pub struct PeerState {
470    pub version: u32,
471    pub services: u64,
472    pub user_agent: String,
473    pub start_height: i32,
474    pub handshake_complete: bool,
475    pub known_addresses: Vec<NetworkAddress>,
476    pub ping_nonce: Option<u64>,
477    pub last_pong: Option<std::time::SystemTime>,
478    pub min_fee_rate: Option<u64>,
479    /// BIP130: peer prefers `headers` over `inv` for block announcements.
480    pub prefer_headers: bool,
481    /// BIP152: negotiated compact-block version (1 or 2), if peer sent sendcmpct.
482    pub compact_block_version: Option<u8>,
483    /// BIP152: peer prefers compact blocks when true.
484    pub prefer_compact_block: bool,
485    /// BIP324: v2 encrypted transport state (if enabled)
486    /// Note: V2Transport is not Clone, so we use Option<Box<V2Transport>> for storage
487    #[cfg(all(
488        feature = "bip324",
489        any(target_arch = "x86_64", target_arch = "aarch64")
490    ))]
491    pub v2_transport: Option<std::sync::Arc<crate::v2_transport::V2Transport>>,
492    /// BIP324: Whether v2 transport handshake is in progress
493    #[cfg(all(
494        feature = "bip324",
495        any(target_arch = "x86_64", target_arch = "aarch64")
496    ))]
497    pub v2_handshake: Option<std::sync::Arc<crate::v2_transport::V2Handshake>>,
498}
499
500impl PeerState {
501    pub fn new() -> Self {
502        Self {
503            version: 0,
504            services: 0,
505            user_agent: String::new(),
506            start_height: 0,
507            handshake_complete: false,
508            known_addresses: Vec::new(),
509            ping_nonce: None,
510            last_pong: None,
511            min_fee_rate: None,
512            prefer_headers: false,
513            compact_block_version: None,
514            prefer_compact_block: false,
515            #[cfg(all(
516                feature = "bip324",
517                any(target_arch = "x86_64", target_arch = "aarch64")
518            ))]
519            v2_transport: None,
520            #[cfg(all(
521                feature = "bip324",
522                any(target_arch = "x86_64", target_arch = "aarch64")
523            ))]
524            v2_handshake: None,
525        }
526    }
527
528    /// Check if peer supports BIP324 v2 encrypted transport
529    #[cfg(all(
530        feature = "bip324",
531        any(target_arch = "x86_64", target_arch = "aarch64")
532    ))]
533    pub fn supports_v2_transport(&self) -> bool {
534        use crate::service_flags::{has_flag, standard};
535        has_flag(self.services, standard::NODE_V2_TRANSPORT)
536    }
537
538    /// Check if v2 transport is active
539    #[cfg(all(
540        feature = "bip324",
541        any(target_arch = "x86_64", target_arch = "aarch64")
542    ))]
543    pub fn is_v2_transport_active(&self) -> bool {
544        self.v2_transport.is_some()
545    }
546}
547
548impl Default for PeerState {
549    fn default() -> Self {
550        Self::new()
551    }
552}
553
554/// Chain object (block or transaction)
555#[derive(Debug, Clone)]
556pub enum ChainObject {
557    Block(Arc<Block>),
558    Transaction(Arc<Transaction>),
559}
560
561impl ChainObject {
562    pub fn as_block(&self) -> Option<&Arc<Block>> {
563        match self {
564            ChainObject::Block(block) => Some(block),
565            _ => None,
566        }
567    }
568
569    pub fn as_transaction(&self) -> Option<&Arc<Transaction>> {
570        match self {
571            ChainObject::Transaction(tx) => Some(tx),
572            _ => None,
573        }
574    }
575}
576
577/// Trait for chain state access (node layer implements this)
578///
579/// This trait allows the protocol layer to query chain state without
580/// owning it. The node layer provides real implementations using its
581/// storage modules (BlockStore, TxIndex, MempoolManager).
582pub trait ChainStateAccess {
583    /// Check if we have an object (block or transaction) by hash
584    fn has_object(&self, hash: &Hash) -> bool;
585
586    /// Get an object (block or transaction) by hash
587    fn get_object(&self, hash: &Hash) -> Option<ChainObject>;
588
589    /// Get headers for a block locator (for GetHeaders requests)
590    /// This implements the Bitcoin block locator algorithm
591    fn get_headers_for_locator(&self, locator: &[Hash], stop: &Hash) -> Vec<BlockHeader>;
592
593    /// Get all mempool transactions
594    fn get_mempool_transactions(&self) -> Vec<Transaction>;
595
596    /// Witness stacks for a stored block (SegWit relay). Default: not available.
597    fn get_block_witnesses(
598        &self,
599        _hash: &Hash,
600    ) -> Option<Vec<Vec<blvm_consensus::segwit::Witness>>> {
601        None
602    }
603}
604
605/// Process incoming network message
606///
607/// This function handles Bitcoin P2P protocol messages, applying protocol-specific
608/// limits and delegating consensus validation to the protocol engine.
609///
610/// # Arguments
611///
612/// * `engine` - The protocol engine (contains consensus layer)
613/// * `message` - The network message to process
614/// * `peer_state` - Current peer connection state
615/// * `chain_access` - Optional chain state access (node layer provides this)
616/// * `utxo_set` - Optional UTXO set for block validation
617/// * `height` - Optional block height for validation context
618///
619/// # Returns
620///
621/// A `NetworkResponse` indicating the result of processing
622pub fn process_network_message(
623    engine: &BitcoinProtocolEngine,
624    message: &NetworkMessage,
625    peer_state: &mut PeerState,
626    chain_access: Option<&dyn ChainStateAccess>,
627    utxo_set: Option<&UtxoSet>,
628    height: Option<u64>,
629) -> Result<NetworkResponse> {
630    let config = engine.get_config();
631    match message {
632        NetworkMessage::Version(version) => process_version_message(version, peer_state, config),
633        NetworkMessage::VerAck => process_verack_message(peer_state),
634        NetworkMessage::Addr(addr) => process_addr_message(addr, peer_state, config),
635        NetworkMessage::AddrV2(addrv2) => process_addrv2_message(addrv2, peer_state, config),
636        NetworkMessage::Inv(inv) => process_inv_message(inv, chain_access, config),
637        NetworkMessage::GetData(getdata) => process_getdata_message(getdata, chain_access, config),
638        NetworkMessage::GetHeaders(getheaders) => {
639            process_getheaders_message(getheaders, chain_access, config)
640        }
641        NetworkMessage::Headers(headers) => process_headers_message(headers, config),
642        NetworkMessage::Block(block, witnesses) => {
643            process_block_message(engine, block, witnesses, utxo_set, height, config)
644        }
645        NetworkMessage::Tx(tx) => process_tx_message(engine, tx, height),
646        NetworkMessage::Ping(ping) => process_ping_message(ping, peer_state),
647        NetworkMessage::Pong(pong) => process_pong_message(pong, peer_state),
648        NetworkMessage::MemPool => process_mempool_message(chain_access),
649        NetworkMessage::FeeFilter(feefilter) => process_feefilter_message(feefilter, peer_state),
650        NetworkMessage::GetBlocks(getblocks) => {
651            process_getblocks_message(getblocks, chain_access, config)
652        }
653        NetworkMessage::GetAddr => process_getaddr_message(peer_state, config),
654        NetworkMessage::NotFound(notfound) => process_notfound_message(notfound, config),
655        NetworkMessage::Reject(reject) => process_reject_message(reject, config),
656        NetworkMessage::SendHeaders => process_sendheaders_message(peer_state),
657        NetworkMessage::SendCmpct(sendcmpct) => {
658            process_sendcmpct_message(sendcmpct, peer_state, config)
659        }
660        NetworkMessage::CmpctBlock(cmpctblock) => process_cmpctblock_message(cmpctblock),
661        NetworkMessage::GetBlockTxn(getblocktxn) => {
662            process_getblocktxn_message(getblocktxn, chain_access, config)
663        }
664        NetworkMessage::BlockTxn(blocktxn) => process_blocktxn_message(blocktxn),
665        #[cfg(feature = "utxo-commitments")]
666        NetworkMessage::GetUTXOSet(getutxoset) => process_getutxoset_message(getutxoset),
667        #[cfg(feature = "utxo-commitments")]
668        NetworkMessage::UTXOSet(utxoset) => process_utxoset_message(utxoset),
669        #[cfg(feature = "utxo-commitments")]
670        NetworkMessage::GetFilteredBlock(getfiltered) => {
671            process_getfilteredblock_message(getfiltered)
672        }
673        #[cfg(feature = "utxo-commitments")]
674        NetworkMessage::FilteredBlock(filtered) => process_filteredblock_message(filtered),
675        NetworkMessage::GetBanList(getbanlist) => process_getbanlist_message(getbanlist),
676        NetworkMessage::BanList(banlist) => process_banlist_message(banlist),
677    }
678}
679
680/// Process version message
681fn process_version_message(
682    version: &VersionMessage,
683    peer_state: &mut PeerState,
684    config: &ProtocolConfig,
685) -> Result<NetworkResponse> {
686    // Validate version message
687    if version.version < 70001 {
688        return Ok(NetworkResponse::Reject("Version too old".into()));
689    }
690
691    // Validate user agent length (from config)
692    if version.user_agent.len() > config.network_limits.max_user_agent_length {
693        return Ok(NetworkResponse::Reject(format!(
694            "User agent too long (max {} bytes)",
695            config.network_limits.max_user_agent_length
696        )));
697    }
698
699    // Update peer state
700    peer_state.version = version.version;
701    peer_state.services = version.services;
702    peer_state.user_agent = version.user_agent.clone();
703    peer_state.start_height = version.start_height;
704
705    // BIP324: Check if peer supports v2 transport and we should negotiate it
706    #[cfg(all(
707        feature = "bip324",
708        any(target_arch = "x86_64", target_arch = "aarch64")
709    ))]
710    {
711        use crate::service_flags::{has_flag, standard};
712        let peer_supports_v2 = has_flag(version.services, standard::NODE_V2_TRANSPORT);
713        let we_support_v2 = config.service_flags.node_v2_transport;
714
715        if peer_supports_v2 && we_support_v2 {
716            // Initiate v2 handshake (responder side)
717            let handshake = crate::v2_transport::V2Handshake::new_responder();
718            peer_state.v2_handshake = Some(std::sync::Arc::new(handshake));
719            // Note: Actual handshake happens at connection level, not in version message
720            // This just records that we should use v2 transport
721        }
722    }
723
724    // Send verack response
725    Ok(NetworkResponse::SendMessage(Box::new(
726        NetworkMessage::VerAck,
727    )))
728}
729
730/// Process verack message
731fn process_verack_message(peer_state: &mut PeerState) -> Result<NetworkResponse> {
732    peer_state.handshake_complete = true;
733    Ok(NetworkResponse::Ok)
734}
735
736/// Process addr message
737fn process_addr_message(
738    addr: &AddrMessage,
739    peer_state: &mut PeerState,
740    config: &ProtocolConfig,
741) -> Result<NetworkResponse> {
742    // Validate address count (from config)
743    if addr.addresses.len() > config.network_limits.max_addr_addresses {
744        return Ok(NetworkResponse::Reject(format!(
745            "Too many addresses (max {})",
746            config.network_limits.max_addr_addresses
747        )));
748    }
749
750    // Store addresses for future use
751    peer_state.known_addresses.extend(addr.addresses.clone());
752
753    Ok(NetworkResponse::Ok)
754}
755
756/// Process addrv2 message (BIP155)
757fn process_addrv2_message(
758    addrv2: &AddrV2Message,
759    peer_state: &mut PeerState,
760    config: &ProtocolConfig,
761) -> Result<NetworkResponse> {
762    // Validate address count (from config)
763    if addrv2.addresses.len() > config.network_limits.max_addr_addresses {
764        return Ok(NetworkResponse::Reject(format!(
765            "Too many addresses (max {})",
766            config.network_limits.max_addr_addresses
767        )));
768    }
769
770    // Convert addrv2 addresses to legacy format where possible and store
771    for addr_v2 in &addrv2.addresses {
772        if let Some(legacy_addr) = addr_v2.to_legacy() {
773            peer_state.known_addresses.push(legacy_addr);
774        }
775        // Note: Tor v3, I2P, CJDNS addresses cannot be converted to legacy format
776        // but we still process them (they're stored in addrv2 format in node layer)
777    }
778
779    Ok(NetworkResponse::Ok)
780}
781
782/// Process inv message
783fn process_inv_message(
784    inv: &InvMessage,
785    chain_access: Option<&dyn ChainStateAccess>,
786    config: &ProtocolConfig,
787) -> Result<NetworkResponse> {
788    // Validate inventory count (from config)
789    if inv.inventory.len() > config.network_limits.max_inv_items {
790        return Ok(NetworkResponse::Reject(format!(
791            "Too many inventory items (max {})",
792            config.network_limits.max_inv_items
793        )));
794    }
795
796    // Check which items we need (if chain access provided)
797    if let Some(chain) = chain_access {
798        let mut needed_items = Vec::with_capacity(inv.inventory.len());
799        for item in &inv.inventory {
800            if !chain.has_object(&item.hash) {
801                needed_items.push(item.clone());
802            }
803        }
804
805        if !needed_items.is_empty() {
806            return Ok(NetworkResponse::SendMessage(Box::new(
807                NetworkMessage::GetData(GetDataMessage {
808                    inventory: needed_items,
809                }),
810            )));
811        }
812    }
813
814    Ok(NetworkResponse::Ok)
815}
816
817/// Process getdata message
818fn process_getdata_message(
819    getdata: &GetDataMessage,
820    chain_access: Option<&dyn ChainStateAccess>,
821    config: &ProtocolConfig,
822) -> Result<NetworkResponse> {
823    // Validate request count (from config)
824    if getdata.inventory.len() > config.network_limits.max_inv_items {
825        return Ok(NetworkResponse::Reject(format!(
826            "Too many getdata items (max {})",
827            config.network_limits.max_inv_items
828        )));
829    }
830
831    // Send requested objects (if chain access provided)
832    if let Some(chain) = chain_access {
833        let mut responses = Vec::with_capacity(getdata.inventory.len());
834        for item in &getdata.inventory {
835            if let Some(obj) = chain.get_object(&item.hash) {
836                match item.inv_type {
837                    1 => {
838                        // MSG_TX
839                        if let Some(tx) = obj.as_transaction() {
840                            responses.push(NetworkMessage::Tx(Arc::clone(tx)));
841                        }
842                    }
843                    2 => {
844                        // MSG_BLOCK
845                        if let Some(block) = obj.as_block() {
846                            let Some(witnesses) = chain.get_block_witnesses(&item.hash) else {
847                                // REV-P-08: do not synthesize empty witness stacks when storage
848                                // has no witness data (node layer serves notfound instead).
849                                continue;
850                            };
851                            responses.push(NetworkMessage::Block(Arc::clone(block), witnesses));
852                        }
853                    }
854                    _ => {
855                        // Unknown inventory type - skip
856                    }
857                }
858            }
859        }
860
861        if !responses.is_empty() {
862            return Ok(NetworkResponse::SendMessages(responses));
863        }
864    }
865
866    Ok(NetworkResponse::Ok)
867}
868
869/// Process getheaders message
870fn process_getheaders_message(
871    getheaders: &GetHeadersMessage,
872    chain_access: Option<&dyn ChainStateAccess>,
873    config: &ProtocolConfig,
874) -> Result<NetworkResponse> {
875    // Validate block locator size (from config)
876    if getheaders.block_locator_hashes.len() > config.validation.max_locator_hashes {
877        return Ok(NetworkResponse::Reject(format!(
878            "Too many locator hashes (max {})",
879            config.validation.max_locator_hashes
880        )));
881    }
882
883    // Use chain access to find headers (if provided)
884    if let Some(chain) = chain_access {
885        let headers =
886            chain.get_headers_for_locator(&getheaders.block_locator_hashes, &getheaders.hash_stop);
887        return Ok(NetworkResponse::SendMessage(Box::new(
888            NetworkMessage::Headers(HeadersMessage { headers }),
889        )));
890    }
891
892    Ok(NetworkResponse::Reject("Chain access not available".into()))
893}
894
895/// Process headers message
896fn process_headers_message(
897    headers: &HeadersMessage,
898    config: &ProtocolConfig,
899) -> Result<NetworkResponse> {
900    // Validate header count (from config)
901    if headers.headers.len() > config.network_limits.max_headers {
902        return Ok(NetworkResponse::Reject(format!(
903            "Too many headers (max {})",
904            config.network_limits.max_headers
905        )));
906    }
907
908    // Header validation is consensus logic, not protocol
909    // Node layer will validate headers using consensus layer
910    Ok(NetworkResponse::Ok)
911}
912
913/// Process block message
914fn process_block_message(
915    engine: &BitcoinProtocolEngine,
916    block: &Block,
917    witnesses: &[Vec<blvm_consensus::segwit::Witness>],
918    utxo_set: Option<&UtxoSet>,
919    height: Option<u64>,
920    config: &ProtocolConfig,
921) -> Result<NetworkResponse> {
922    // Check protocol limits first (from config)
923    if block.transactions.len() > config.validation.max_txs_per_block {
924        return Err(crate::error::ProtocolError::MessageTooLarge {
925            size: block.transactions.len(),
926            max: config.validation.max_txs_per_block,
927        });
928    }
929
930    // Delegate to consensus via protocol engine (requires utxo_set and height)
931    if let (Some(utxos), Some(h)) = (utxo_set, height) {
932        let context = ProtocolValidationContext::new(engine.get_protocol_version(), h)?;
933        // Pass witnesses for full BIP141/Taproot weight and script validation.
934        let result = engine
935            .validate_block_with_protocol_and_witnesses(block, witnesses, utxos, h, &context)?;
936
937        match result {
938            ValidationResult::Valid => Ok(NetworkResponse::Ok),
939            ValidationResult::Invalid(reason) => {
940                Ok(NetworkResponse::Reject(format!("Invalid block: {reason}")))
941            }
942        }
943    } else {
944        Err(crate::error::ProtocolError::Configuration(
945            "Missing validation context (utxo_set and height required)".into(),
946        ))
947    }
948}
949
950/// Process transaction message
951fn process_tx_message(
952    engine: &BitcoinProtocolEngine,
953    tx: &Transaction,
954    height: Option<u64>,
955) -> Result<NetworkResponse> {
956    // Check protocol limits and validate
957    let context =
958        ProtocolValidationContext::new(engine.get_protocol_version(), height.unwrap_or(0))?;
959    let result = engine.validate_transaction_with_protocol(tx, &context)?;
960
961    match result {
962        ValidationResult::Valid => Ok(NetworkResponse::Ok),
963        ValidationResult::Invalid(reason) => Ok(NetworkResponse::Reject(format!(
964            "Invalid transaction: {reason}"
965        ))),
966    }
967}
968
969/// Process ping message
970fn process_ping_message(
971    ping: &PingMessage,
972    _peer_state: &mut PeerState,
973) -> Result<NetworkResponse> {
974    let pong = NetworkMessage::Pong(PongMessage { nonce: ping.nonce });
975    Ok(NetworkResponse::SendMessage(Box::new(pong)))
976}
977
978/// Process pong message
979fn process_pong_message(pong: &PongMessage, peer_state: &mut PeerState) -> Result<NetworkResponse> {
980    // Validate pong nonce matches our ping
981    if peer_state.ping_nonce == Some(pong.nonce) {
982        peer_state.ping_nonce = None;
983        peer_state.last_pong = Some(std::time::SystemTime::now());
984    }
985
986    Ok(NetworkResponse::Ok)
987}
988
989/// Process mempool message
990fn process_mempool_message(chain_access: Option<&dyn ChainStateAccess>) -> Result<NetworkResponse> {
991    // Send all mempool transactions (if chain access provided)
992    if let Some(chain) = chain_access {
993        let mempool_txs = chain.get_mempool_transactions();
994        let mut responses = Vec::with_capacity(mempool_txs.len());
995
996        for tx in mempool_txs {
997            responses.push(NetworkMessage::Tx(Arc::new(tx)));
998        }
999
1000        if !responses.is_empty() {
1001            return Ok(NetworkResponse::SendMessages(responses));
1002        }
1003    }
1004
1005    Ok(NetworkResponse::Ok)
1006}
1007
1008/// Process feefilter message
1009fn process_feefilter_message(
1010    feefilter: &FeeFilterMessage,
1011    peer_state: &mut PeerState,
1012) -> Result<NetworkResponse> {
1013    peer_state.min_fee_rate = Some(feefilter.feerate);
1014    Ok(NetworkResponse::Ok)
1015}
1016
1017/// Process getblocks message
1018fn process_getblocks_message(
1019    getblocks: &GetBlocksMessage,
1020    chain_access: Option<&dyn ChainStateAccess>,
1021    config: &ProtocolConfig,
1022) -> Result<NetworkResponse> {
1023    // Validate block locator size (from config)
1024    if getblocks.block_locator_hashes.len() > config.validation.max_locator_hashes {
1025        return Ok(NetworkResponse::Reject(format!(
1026            "Too many locator hashes (max {})",
1027            config.validation.max_locator_hashes
1028        )));
1029    }
1030
1031    if let Some(chain) = chain_access {
1032        use blvm_consensus::block::block_header_hash;
1033
1034        // Core-compatible: walk locator to fork, then advertise block hashes after fork.
1035        const MAX_GETBLOCKS_INV: usize = 500;
1036        let headers =
1037            chain.get_headers_for_locator(&getblocks.block_locator_hashes, &getblocks.hash_stop);
1038        if headers.is_empty() {
1039            return Ok(NetworkResponse::Ok);
1040        }
1041
1042        let inventory: Vec<InventoryVector> = headers
1043            .into_iter()
1044            .take(MAX_GETBLOCKS_INV)
1045            .map(|header| InventoryVector {
1046                inv_type: 2, // MSG_BLOCK
1047                hash: block_header_hash(&header),
1048            })
1049            .collect();
1050
1051        return Ok(NetworkResponse::SendMessage(Box::new(NetworkMessage::Inv(
1052            InvMessage { inventory },
1053        ))));
1054    }
1055
1056    Ok(NetworkResponse::Ok)
1057}
1058
1059/// Process getaddr message
1060fn process_getaddr_message(
1061    peer_state: &mut PeerState,
1062    config: &ProtocolConfig,
1063) -> Result<NetworkResponse> {
1064    // Return known addresses (if any)
1065    if !peer_state.known_addresses.is_empty() {
1066        // Limit to configured max addresses
1067        let max_addrs = config
1068            .network_limits
1069            .max_addr_addresses
1070            .min(peer_state.known_addresses.len());
1071        let mut addresses = Vec::with_capacity(max_addrs);
1072        addresses.extend(peer_state.known_addresses.iter().take(max_addrs).cloned());
1073
1074        return Ok(NetworkResponse::SendMessage(Box::new(
1075            NetworkMessage::Addr(AddrMessage { addresses }),
1076        )));
1077    }
1078
1079    Ok(NetworkResponse::Ok)
1080}
1081
1082/// Process notfound message
1083fn process_notfound_message(
1084    notfound: &NotFoundMessage,
1085    config: &ProtocolConfig,
1086) -> Result<NetworkResponse> {
1087    // Validate inventory count (from config)
1088    if notfound.inventory.len() > config.network_limits.max_inv_items {
1089        return Ok(NetworkResponse::Reject(format!(
1090            "Too many notfound items (max {})",
1091            config.network_limits.max_inv_items
1092        )));
1093    }
1094
1095    // NotFound is informational - just acknowledge
1096    Ok(NetworkResponse::Ok)
1097}
1098
1099/// Process reject message
1100fn process_reject_message(
1101    reject: &RejectMessage,
1102    _config: &ProtocolConfig,
1103) -> Result<NetworkResponse> {
1104    // Validate message name length (Bitcoin protocol limit: 12 bytes)
1105    // This is a fixed protocol limit, not configurable
1106    if reject.message.len() > 12 {
1107        return Ok(NetworkResponse::Reject(
1108            "Invalid reject message name".into(),
1109        ));
1110    }
1111
1112    // Validate reason length (Bitcoin protocol limit: 111 bytes)
1113    // This is a fixed protocol limit, not configurable
1114    if reject.reason.len() > 111 {
1115        return Ok(NetworkResponse::Reject("Reject reason too long".into()));
1116    }
1117
1118    // Reject is informational - log and acknowledge
1119    // In production, this would trigger appropriate handling (ban peer, etc.)
1120    Ok(NetworkResponse::Ok)
1121}
1122
1123/// Process sendheaders message
1124fn process_sendheaders_message(peer_state: &mut PeerState) -> Result<NetworkResponse> {
1125    peer_state.prefer_headers = true;
1126    Ok(NetworkResponse::Ok)
1127}
1128
1129/// Process sendcmpct message (BIP152)
1130fn process_sendcmpct_message(
1131    sendcmpct: &SendCmpctMessage,
1132    peer_state: &mut PeerState,
1133    config: &ProtocolConfig,
1134) -> Result<NetworkResponse> {
1135    // Validate version (must be 1 or 2, or match configured preferred version)
1136    let valid_versions = [1, 2];
1137    if !valid_versions.contains(&sendcmpct.version) {
1138        return Ok(NetworkResponse::Reject(
1139            "Invalid compact block version".into(),
1140        ));
1141    }
1142
1143    // Check if compact blocks are enabled
1144    if !config.compact_blocks.enabled {
1145        return Ok(NetworkResponse::Reject("Compact blocks not enabled".into()));
1146    }
1147
1148    // Store compact block preference in peer state
1149    peer_state.compact_block_version = Some(sendcmpct.version as u8);
1150    peer_state.prefer_compact_block = sendcmpct.prefer_cmpct != 0;
1151    Ok(NetworkResponse::Ok)
1152}
1153
1154/// Process cmpctblock message (BIP152)
1155fn process_cmpctblock_message(_cmpctblock: &CmpctBlockMessage) -> Result<NetworkResponse> {
1156    Ok(NetworkResponse::Reject(
1157        "Compact block reconstruction not implemented".into(),
1158    ))
1159}
1160
1161/// Process getblocktxn message (BIP152)
1162fn process_getblocktxn_message(
1163    getblocktxn: &GetBlockTxnMessage,
1164    chain_access: Option<&dyn ChainStateAccess>,
1165    config: &ProtocolConfig,
1166) -> Result<NetworkResponse> {
1167    // Validate indices count (from config)
1168    if getblocktxn.indices.len() > config.compact_blocks.max_blocktxn_indices {
1169        return Ok(NetworkResponse::Reject(format!(
1170            "Too many transaction indices (max {})",
1171            config.compact_blocks.max_blocktxn_indices
1172        )));
1173    }
1174
1175    // Use chain access to get requested transactions and witnesses
1176    if let Some(chain) = chain_access {
1177        if let Some(obj) = chain.get_object(&getblocktxn.block_hash) {
1178            if let Some(block) = obj.as_block() {
1179                let stored_witnesses = chain.get_block_witnesses(&getblocktxn.block_hash);
1180                let mut transactions = Vec::with_capacity(getblocktxn.indices.len());
1181                let mut req_witnesses = Vec::with_capacity(getblocktxn.indices.len());
1182
1183                for &index in &getblocktxn.indices {
1184                    let idx = index as usize;
1185                    if idx >= block.transactions.len() {
1186                        return Ok(NetworkResponse::Reject(
1187                            "getblocktxn index past end of block".into(),
1188                        ));
1189                    }
1190                    transactions.push(block.transactions[idx].clone());
1191                    let tx_witness = stored_witnesses
1192                        .as_ref()
1193                        .and_then(|w| w.get(idx).cloned())
1194                        .unwrap_or_else(|| {
1195                            block.transactions[idx]
1196                                .inputs
1197                                .iter()
1198                                .map(|_| Vec::new())
1199                                .collect()
1200                        });
1201                    req_witnesses.push(tx_witness);
1202                }
1203
1204                if !transactions.is_empty() {
1205                    return Ok(NetworkResponse::SendMessage(Box::new(
1206                        NetworkMessage::BlockTxn(BlockTxnMessage {
1207                            block_hash: getblocktxn.block_hash,
1208                            transactions,
1209                            witnesses: Some(req_witnesses),
1210                        }),
1211                    )));
1212                }
1213            }
1214        }
1215    }
1216
1217    Ok(NetworkResponse::Ok)
1218}
1219
1220/// Process blocktxn message (BIP152)
1221fn process_blocktxn_message(_blocktxn: &BlockTxnMessage) -> Result<NetworkResponse> {
1222    Ok(NetworkResponse::Reject(
1223        "Compact block reconstruction not implemented".into(),
1224    ))
1225}
1226
1227#[cfg(feature = "utxo-commitments")]
1228/// Process getutxoset message
1229fn process_getutxoset_message(_getutxoset: &commons::GetUTXOSetMessage) -> Result<NetworkResponse> {
1230    Ok(NetworkResponse::Reject(
1231        "UTXO set relay not implemented".into(),
1232    ))
1233}
1234
1235#[cfg(feature = "utxo-commitments")]
1236/// Process utxoset message
1237fn process_utxoset_message(_utxoset: &commons::UTXOSetMessage) -> Result<NetworkResponse> {
1238    Ok(NetworkResponse::Reject(
1239        "UTXO set relay not implemented".into(),
1240    ))
1241}
1242
1243#[cfg(feature = "utxo-commitments")]
1244/// Process getfilteredblock message
1245fn process_getfilteredblock_message(
1246    _getfiltered: &commons::GetFilteredBlockMessage,
1247) -> Result<NetworkResponse> {
1248    Ok(NetworkResponse::Reject(
1249        "Filtered block relay not implemented".into(),
1250    ))
1251}
1252
1253#[cfg(feature = "utxo-commitments")]
1254/// Process filteredblock message
1255fn process_filteredblock_message(
1256    _filtered: &commons::FilteredBlockMessage,
1257) -> Result<NetworkResponse> {
1258    Ok(NetworkResponse::Reject(
1259        "Filtered block relay not implemented".into(),
1260    ))
1261}
1262
1263/// Process getbanlist message
1264fn process_getbanlist_message(_getbanlist: &commons::GetBanListMessage) -> Result<NetworkResponse> {
1265    Ok(NetworkResponse::Reject(
1266        "Ban list relay not implemented".into(),
1267    ))
1268}
1269
1270/// Process banlist message
1271fn process_banlist_message(_banlist: &commons::BanListMessage) -> Result<NetworkResponse> {
1272    Ok(NetworkResponse::Reject(
1273        "Ban list relay not implemented".into(),
1274    ))
1275}
1276
1277#[cfg(test)]
1278fn assert_getblocktxn_past_end_rejects() {
1279    struct TwoTx;
1280    impl ChainStateAccess for TwoTx {
1281        fn has_object(&self, _hash: &Hash) -> bool {
1282            true
1283        }
1284        fn get_object(&self, hash: &Hash) -> Option<ChainObject> {
1285            let tx = Transaction {
1286                version: 1,
1287                inputs: vec![].into(),
1288                outputs: vec![].into(),
1289                lock_time: 0,
1290            };
1291            let block = Block {
1292                header: BlockHeader {
1293                    version: 1,
1294                    prev_block_hash: [0; 32],
1295                    merkle_root: [0; 32],
1296                    timestamp: 1,
1297                    bits: 1,
1298                    nonce: 1,
1299                },
1300                transactions: vec![tx.clone(), tx].into_boxed_slice(),
1301            };
1302            if hash == &[9u8; 32] {
1303                Some(ChainObject::Block(Arc::new(block)))
1304            } else {
1305                None
1306            }
1307        }
1308        fn get_headers_for_locator(&self, _locator: &[Hash], _stop: &Hash) -> Vec<BlockHeader> {
1309            Vec::new()
1310        }
1311        fn get_mempool_transactions(&self) -> Vec<Transaction> {
1312            Vec::new()
1313        }
1314    }
1315    let msg = GetBlockTxnMessage {
1316        block_hash: [9u8; 32],
1317        indices: vec![5],
1318    };
1319    let response =
1320        process_getblocktxn_message(&msg, Some(&TwoTx), &ProtocolConfig::default()).unwrap();
1321    assert!(
1322        matches!(response, NetworkResponse::Reject(_)),
1323        "expected reject, got {response:?}"
1324    );
1325}
1326
1327#[cfg(test)]
1328#[test]
1329fn getblocktxn_index_past_block_returns_reject_and_no_blocktxn() {
1330    assert_getblocktxn_past_end_rejects();
1331}