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