1use std::{
4 collections::{HashMap, HashSet, VecDeque},
5 fmt,
6 future::Future,
7 io::Cursor,
8 pin::Pin,
9 sync::{
10 atomic::{AtomicU64, Ordering},
11 Arc, Mutex as StdMutex, OnceLock,
12 },
13 task::{Context, Poll},
14 time::Duration,
15};
16
17use serde_json::{Map, Number, Value};
18use thiserror::Error;
19use tokio::{
20 sync::{mpsc, oneshot, Mutex, OwnedSemaphorePermit, Semaphore},
21 time::{sleep, timeout, Instant},
22};
23use tower::{Service, ServiceExt};
24
25use zakura_chain::{
26 block::{self, Block, MAX_BLOCK_LOCATOR_LENGTH},
27 serialization::{
28 CompactSizeMessage, SerializationError, ZcashDeserialize, ZcashSerialize,
29 MAX_HEADERS_PER_MESSAGE, MAX_PROTOCOL_MESSAGE_LEN,
30 },
31 transaction::{Transaction, UnminedTx, UnminedTxId},
32};
33
34use crate::{
35 protocol::{
36 external::InventoryHash,
37 internal::{InventoryResponse, PeerSource, Request, Response},
38 },
39 BoxError, MAX_TX_INV_IN_SENT_MESSAGE,
40};
41
42use super::{
43 spawn_supervised_peer_task, trace::peer_label as trace_peer_label, BoxRunFuture, Frame,
44 FramedSend, OrderedSendError, Peer, RequestResponseService, Service as ZakuraService,
45 SinkReject, Stream, StreamMode, ZakuraConnId, ZakuraPeerHandle, ZakuraPeerId,
46 ZakuraSupervisorHandle, ZakuraTrace, FRAME_HEADER_BYTES, LEGACY_REQUEST_TABLE,
47 LOCAL_MAX_CONTROL_FRAME_BYTES, ZAKURA_CAP_LEGACY_GOSSIP,
48};
49
50pub const ZAKURA_STREAM_GOSSIP: u16 = 2;
52pub const ZAKURA_STREAM_LEGACY_REQUESTS: u16 = 3;
54pub const LEGACY_GOSSIP_VERSION: u16 = 1;
56pub const MSG_ADVERTISE_BLOCK: u16 = 1;
58pub const MSG_ADVERTISE_TX_IDS: u16 = 2;
60pub const MSG_REQUEST_BLOCKS_BY_HASH: u16 = 3;
62pub const MSG_REQUEST_TRANSACTIONS_BY_ID: u16 = 4;
64pub const MSG_RESPONSE_BLOCK: u16 = 5;
66pub const MSG_RESPONSE_TRANSACTION: u16 = 6;
68pub const MSG_RESPONSE_MISSING_BLOCKS: u16 = 7;
70pub const MSG_RESPONSE_MISSING_TRANSACTIONS: u16 = 8;
72pub const MSG_REQUEST_FIND_BLOCKS: u16 = 9;
74pub const MSG_REQUEST_FIND_HEADERS: u16 = 10;
76pub const MSG_REQUEST_MEMPOOL_TRANSACTION_IDS: u16 = 11;
78pub const MSG_REQUEST_PING: u16 = 12;
80pub const MSG_REQUEST_PUSH_TRANSACTION: u16 = 13;
82pub const MSG_RESPONSE_BLOCK_HASHES: u16 = 14;
84pub const MSG_RESPONSE_BLOCK_HEADERS: u16 = 15;
86pub const MSG_RESPONSE_TRANSACTION_IDS: u16 = 16;
88pub const MSG_RESPONSE_PONG: u16 = 17;
90pub const MSG_RESPONSE_NIL: u16 = 18;
92
93const LEGACY_GOSSIP_INBOUND_QUEUE: usize = 256;
94const LEGACY_REQUEST_IN_FLIGHT_LIMIT: usize = 64;
95const LEGACY_GOSSIP_SERVICE_TIMEOUT: Duration = Duration::from_secs(30);
96const DEFAULT_FIRST_SEEN_TTL: Duration = Duration::from_secs(10 * 60);
97const DEFAULT_FIRST_SEEN_CAPACITY: usize = 50_000;
98const LEGACY_GOSSIP_EXPENSIVE_ATTEMPT: Duration = Duration::from_secs(1);
107const LEGACY_GOSSIP_DUPLICATE_COOLDOWN: Duration = Duration::from_secs(30);
110const LEGACY_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
111const SOURCE_INVENTORY_MISSING_RETRIES: usize = 8;
112const SOURCE_INVENTORY_MISSING_RETRY_DELAY: Duration = Duration::from_millis(500);
113const LEGACY_REQUEST_READY_TIMEOUT: Duration = Duration::from_secs(10);
114const DUAL_STACK_LEGACY_INVENTORY_TIMEOUT: Duration = Duration::from_secs(3);
119const LEGACY_RESPONSE_CHUNK_BYTES: usize = 512 * 1024;
120const LEGACY_RESPONSE_MAX_AGGREGATE_BYTES: usize = 8 * MAX_PROTOCOL_MESSAGE_LEN;
132const REQUEST_ID_BYTES: usize = 8;
133const RESPONSE_CHUNK_HEADER_BYTES: usize = REQUEST_ID_BYTES + 1;
134const NO_STOP_HASH: block::Hash = block::Hash([0; 32]);
135const LEGACY_GOSSIP_SERVICE_STREAMS: [Stream; 2] = [
136 Stream {
137 kind: ZAKURA_STREAM_GOSSIP,
138 version: LEGACY_GOSSIP_VERSION,
139 frame_cap: LOCAL_MAX_CONTROL_FRAME_BYTES,
142 capability: ZAKURA_CAP_LEGACY_GOSSIP,
143 mode: StreamMode::Ordered,
144 },
145 Stream {
146 kind: ZAKURA_STREAM_LEGACY_REQUESTS,
147 version: LEGACY_GOSSIP_VERSION,
148 frame_cap: LOCAL_MAX_CONTROL_FRAME_BYTES,
151 capability: ZAKURA_CAP_LEGACY_GOSSIP,
152 mode: StreamMode::RequestResponse,
153 },
154];
155
156pub(crate) fn legacy_gossip_streams() -> &'static [Stream] {
158 &LEGACY_GOSSIP_SERVICE_STREAMS
159}
160
161static FIRST_SEEN_BY_SUPERVISOR: OnceLock<std::sync::Mutex<HashMap<u64, FirstSeenCache>>> =
162 OnceLock::new();
163static NEXT_LEGACY_REQUEST_ID: AtomicU64 = AtomicU64::new(1);
164
165#[derive(Clone, Debug, Eq, PartialEq)]
167pub enum LegacyGossipFrame {
168 AdvertiseBlock(block::Hash),
170 AdvertiseTransactionIds(Vec<UnminedTxId>),
172}
173
174impl LegacyGossipFrame {
175 pub fn from_request(request: Request) -> Result<Self, LegacyGossipError> {
177 match request {
178 Request::AdvertiseBlock(hash, _) | Request::AdvertiseBlockToAll(hash) => {
179 Ok(Self::AdvertiseBlock(hash))
180 }
181 Request::AdvertiseTransactionIds(ids, _) => Self::advertise_transaction_ids(ids),
182 request => Err(LegacyGossipError::UnsupportedRequest(request.command())),
183 }
184 }
185
186 pub fn advertise_transaction_ids(
188 ids: impl IntoIterator<Item = UnminedTxId>,
189 ) -> Result<Self, LegacyGossipError> {
190 let ids = collect_outbound_tx_ids(ids)?;
191 Ok(Self::AdvertiseTransactionIds(ids))
192 }
193
194 pub fn encode_frame(&self) -> Result<Frame, LegacyGossipError> {
196 match self {
197 Self::AdvertiseBlock(hash) => {
198 let mut payload = Vec::new();
199 hash.zcash_serialize(&mut payload)?;
200 Ok(Frame {
201 message_type: MSG_ADVERTISE_BLOCK,
202 flags: 0,
203 payload,
204 })
205 }
206 Self::AdvertiseTransactionIds(ids) => {
207 let ids = outbound_tx_id_slice(ids)?;
208 let mut payload = Vec::new();
209 write_tx_id_list(&mut payload, ids)?;
210 Ok(Frame {
211 message_type: MSG_ADVERTISE_TX_IDS,
212 flags: 0,
213 payload,
214 })
215 }
216 }
217 }
218
219 pub fn decode_frame(frame: Frame) -> Result<Self, LegacyGossipError> {
221 if frame.flags != 0 {
222 return Err(LegacyGossipError::UnsupportedFlags(frame.flags));
223 }
224
225 match frame.message_type {
226 MSG_ADVERTISE_BLOCK => {
227 let mut reader = Cursor::new(frame.payload.as_slice());
228 let hash = block::Hash::zcash_deserialize(&mut reader)?;
229 reject_trailing(&reader)?;
230 Ok(Self::AdvertiseBlock(hash))
231 }
232 MSG_ADVERTISE_TX_IDS => {
233 let mut reader = Cursor::new(frame.payload.as_slice());
234 let ids = read_tx_id_list(&mut reader)?;
235 if ids.is_empty() {
236 return Err(LegacyGossipError::EmptyTransactionAdvertisement);
237 }
238 reject_trailing(&reader)?;
239 Ok(Self::AdvertiseTransactionIds(ids))
240 }
241 message_type => Err(LegacyGossipError::UnknownMessageType(message_type)),
242 }
243 }
244
245 fn into_request(self, peer_id: ZakuraPeerId) -> Request {
246 match self {
247 Self::AdvertiseBlock(hash) => {
248 Request::AdvertiseBlock(hash, Some(PeerSource::Zakura(peer_id)))
249 }
250 Self::AdvertiseTransactionIds(ids) => Request::AdvertiseTransactionIds(
251 ids.into_iter().collect(),
252 Some(PeerSource::Zakura(peer_id)),
253 ),
254 }
255 }
256}
257
258#[derive(Clone, Debug, Eq, PartialEq)]
260pub enum LegacyRequestFrame {
261 BlocksByHash(Vec<block::Hash>),
263 TransactionsById(Vec<UnminedTxId>),
265 FindBlocks {
267 known_blocks: Vec<block::Hash>,
269 stop: Option<block::Hash>,
271 },
272 FindHeaders {
274 known_blocks: Vec<block::Hash>,
276 stop: Option<block::Hash>,
278 },
279 MempoolTransactionIds,
281 Ping,
283 PushTransaction(UnminedTx),
285}
286
287impl LegacyRequestFrame {
288 pub fn from_request(request: Request) -> Result<Self, LegacyGossipError> {
290 match request {
291 Request::BlocksByHash(hashes) | Request::BlocksByHashFrom { hashes, .. } => {
292 let hashes = truncate_to_inventory_cap(hashes)?;
293 Ok(Self::BlocksByHash(hashes))
294 }
295 Request::TransactionsById(ids) | Request::TransactionsByIdFrom { ids, .. } => {
296 let ids = truncate_to_inventory_cap(ids)?;
297 Ok(Self::TransactionsById(ids))
298 }
299 Request::FindBlocks { known_blocks, stop } => {
300 ensure_block_locator_count(known_blocks.len())?;
301 Ok(Self::FindBlocks { known_blocks, stop })
302 }
303 Request::FindHeaders { known_blocks, stop } => {
304 ensure_block_locator_count(known_blocks.len())?;
305 Ok(Self::FindHeaders { known_blocks, stop })
306 }
307 Request::MempoolTransactionIds => Ok(Self::MempoolTransactionIds),
308 Request::Ping(_) => Ok(Self::Ping),
309 Request::PushTransaction(transaction, _) => Ok(Self::PushTransaction(transaction)),
310 request => Err(LegacyGossipError::UnsupportedRequest(request.command())),
311 }
312 }
313
314 pub fn encode_frame(&self) -> Result<Frame, LegacyGossipError> {
316 match self {
317 Self::BlocksByHash(hashes) => {
318 let mut payload = Vec::new();
319 write_hash_list(&mut payload, hashes)?;
320 Ok(Frame {
321 message_type: MSG_REQUEST_BLOCKS_BY_HASH,
322 flags: 0,
323 payload,
324 })
325 }
326 Self::TransactionsById(ids) => {
327 let mut payload = Vec::new();
328 write_tx_id_list(&mut payload, ids)?;
329 Ok(Frame {
330 message_type: MSG_REQUEST_TRANSACTIONS_BY_ID,
331 flags: 0,
332 payload,
333 })
334 }
335 Self::FindBlocks { known_blocks, stop } => {
336 let mut payload = Vec::new();
337 write_block_locator(&mut payload, known_blocks, *stop)?;
338 Ok(Frame {
339 message_type: MSG_REQUEST_FIND_BLOCKS,
340 flags: 0,
341 payload,
342 })
343 }
344 Self::FindHeaders { known_blocks, stop } => {
345 let mut payload = Vec::new();
346 write_block_locator(&mut payload, known_blocks, *stop)?;
347 Ok(Frame {
348 message_type: MSG_REQUEST_FIND_HEADERS,
349 flags: 0,
350 payload,
351 })
352 }
353 Self::MempoolTransactionIds => Ok(Frame {
354 message_type: MSG_REQUEST_MEMPOOL_TRANSACTION_IDS,
355 flags: 0,
356 payload: Vec::new(),
357 }),
358 Self::Ping => Ok(Frame {
359 message_type: MSG_REQUEST_PING,
360 flags: 0,
361 payload: Vec::new(),
362 }),
363 Self::PushTransaction(transaction) => Ok(Frame {
364 message_type: MSG_REQUEST_PUSH_TRANSACTION,
365 flags: 0,
366 payload: transaction.transaction.zcash_serialize_to_vec()?,
367 }),
368 }
369 }
370
371 pub fn decode_frame(frame: Frame) -> Result<Self, LegacyGossipError> {
373 if frame.flags != 0 {
374 return Err(LegacyGossipError::UnsupportedFlags(frame.flags));
375 }
376
377 match frame.message_type {
378 MSG_REQUEST_BLOCKS_BY_HASH => {
379 let mut reader = Cursor::new(frame.payload.as_slice());
380 let hashes = read_hash_list(&mut reader)?;
381 reject_trailing(&reader)?;
382 Ok(Self::BlocksByHash(hashes))
383 }
384 MSG_REQUEST_TRANSACTIONS_BY_ID => {
385 let mut reader = Cursor::new(frame.payload.as_slice());
386 let ids = read_tx_id_list(&mut reader)?;
387 reject_trailing(&reader)?;
388 Ok(Self::TransactionsById(ids))
389 }
390 MSG_REQUEST_FIND_BLOCKS => {
391 let mut reader = Cursor::new(frame.payload.as_slice());
392 let (known_blocks, stop) = read_block_locator(&mut reader)?;
393 reject_trailing(&reader)?;
394 Ok(Self::FindBlocks { known_blocks, stop })
395 }
396 MSG_REQUEST_FIND_HEADERS => {
397 let mut reader = Cursor::new(frame.payload.as_slice());
398 let (known_blocks, stop) = read_block_locator(&mut reader)?;
399 reject_trailing(&reader)?;
400 Ok(Self::FindHeaders { known_blocks, stop })
401 }
402 MSG_REQUEST_MEMPOOL_TRANSACTION_IDS => {
403 if !frame.payload.is_empty() {
404 return Err(LegacyGossipError::TrailingBytes);
405 }
406 Ok(Self::MempoolTransactionIds)
407 }
408 MSG_REQUEST_PING => {
409 if !frame.payload.is_empty() {
410 return Err(LegacyGossipError::TrailingBytes);
411 }
412 Ok(Self::Ping)
413 }
414 MSG_REQUEST_PUSH_TRANSACTION => {
415 let mut reader = Cursor::new(frame.payload.as_slice());
416 let transaction = Transaction::zcash_deserialize(&mut reader)?;
417 reject_trailing(&reader)?;
418 Ok(Self::PushTransaction(UnminedTx::from(transaction)))
419 }
420 message_type => Err(LegacyGossipError::UnknownMessageType(message_type)),
421 }
422 }
423
424 fn into_service_request(self, peer_id: ZakuraPeerId) -> Option<Request> {
425 match self {
426 Self::BlocksByHash(hashes) => Some(Request::BlocksByHash(hashes.into_iter().collect())),
427 Self::TransactionsById(ids) => {
428 Some(Request::TransactionsById(ids.into_iter().collect()))
429 }
430 Self::FindBlocks { known_blocks, stop } => {
431 Some(Request::FindBlocks { known_blocks, stop })
432 }
433 Self::FindHeaders { known_blocks, stop } => {
434 Some(Request::FindHeaders { known_blocks, stop })
435 }
436 Self::MempoolTransactionIds => Some(Request::MempoolTransactionIds),
437 Self::Ping => None,
438 Self::PushTransaction(transaction) => Some(Request::PushTransaction(
439 transaction,
440 Some(PeerSource::Zakura(peer_id)),
441 )),
442 }
443 }
444
445 fn kind(&self) -> LegacyRequestKind {
446 match self {
447 Self::BlocksByHash(_) => LegacyRequestKind::Blocks,
448 Self::TransactionsById(_) => LegacyRequestKind::Transactions,
449 Self::FindBlocks { .. } => LegacyRequestKind::FindBlocks,
450 Self::FindHeaders { .. } => LegacyRequestKind::FindHeaders,
451 Self::MempoolTransactionIds => LegacyRequestKind::MempoolTransactionIds,
452 Self::Ping => LegacyRequestKind::Ping,
453 Self::PushTransaction(_) => LegacyRequestKind::PushTransaction,
454 }
455 }
456}
457
458#[derive(Copy, Clone, Debug, Eq, PartialEq)]
459pub(super) enum LegacyRequestKind {
460 Blocks,
461 Transactions,
462 FindBlocks,
463 FindHeaders,
464 MempoolTransactionIds,
465 Ping,
466 PushTransaction,
467}
468
469impl LegacyRequestKind {
470 fn command(self) -> &'static str {
471 match self {
472 LegacyRequestKind::Blocks => "BlocksByHash",
473 LegacyRequestKind::Transactions => "TransactionsById",
474 LegacyRequestKind::FindBlocks => "FindBlocks",
475 LegacyRequestKind::FindHeaders => "FindHeaders",
476 LegacyRequestKind::MempoolTransactionIds => "MempoolTransactionIds",
477 LegacyRequestKind::Ping => "Ping",
478 LegacyRequestKind::PushTransaction => "PushTransaction",
479 }
480 }
481
482 fn message_type(self) -> u16 {
483 match self {
484 LegacyRequestKind::Blocks => MSG_REQUEST_BLOCKS_BY_HASH,
485 LegacyRequestKind::Transactions => MSG_REQUEST_TRANSACTIONS_BY_ID,
486 LegacyRequestKind::FindBlocks => MSG_REQUEST_FIND_BLOCKS,
487 LegacyRequestKind::FindHeaders => MSG_REQUEST_FIND_HEADERS,
488 LegacyRequestKind::MempoolTransactionIds => MSG_REQUEST_MEMPOOL_TRANSACTION_IDS,
489 LegacyRequestKind::Ping => MSG_REQUEST_PING,
490 LegacyRequestKind::PushTransaction => MSG_REQUEST_PUSH_TRANSACTION,
491 }
492 }
493}
494
495pub(super) struct LegacyResponseCodec;
496
497impl LegacyResponseCodec {
498 pub(super) fn encode_response(
499 request_id: u64,
500 response: Response,
501 max_frame_bytes: u32,
502 max_message_bytes: u32,
503 ) -> Result<Vec<Frame>, LegacyGossipError> {
504 let mut frames = Vec::new();
505 let mut budget = ResponseEncodeBudget::default();
510 match response {
511 Response::Blocks(blocks) => {
512 let mut missing = Vec::new();
513 for block in blocks {
514 match block {
515 InventoryResponse::Available((block, _)) => {
516 push_chunked_response(
517 &mut frames,
518 &mut budget,
519 MSG_RESPONSE_BLOCK,
520 request_id,
521 max_frame_bytes,
522 max_message_bytes,
523 block.zcash_serialize_to_vec()?,
524 )?;
525 }
526 InventoryResponse::Missing(hash) => missing.push(hash),
527 }
528 }
529 if !missing.is_empty() {
530 push_response_frame(
531 &mut frames,
532 &mut budget,
533 missing_blocks_frame(request_id, missing)?,
534 )?;
535 }
536 }
537 Response::Transactions(transactions) => {
538 let mut missing = Vec::new();
539 for transaction in transactions {
540 match transaction {
541 InventoryResponse::Available((transaction, _)) => {
542 push_chunked_response(
543 &mut frames,
544 &mut budget,
545 MSG_RESPONSE_TRANSACTION,
546 request_id,
547 max_frame_bytes,
548 max_message_bytes,
549 transaction.transaction.zcash_serialize_to_vec()?,
550 )?;
551 }
552 InventoryResponse::Missing(id) => missing.push(id),
553 }
554 }
555 if !missing.is_empty() {
556 push_response_frame(
557 &mut frames,
558 &mut budget,
559 missing_transactions_frame(request_id, missing)?,
560 )?;
561 }
562 }
563 Response::BlockHashes(hashes) => {
564 push_response_frame(
566 &mut frames,
567 &mut budget,
568 block_hashes_frame(request_id, hashes)?,
569 )?;
570 }
571 Response::BlockHeaders(headers) => {
572 push_response_frame(
574 &mut frames,
575 &mut budget,
576 block_headers_frame(request_id, headers)?,
577 )?;
578 }
579 Response::TransactionIds(ids) => {
580 push_response_frame(
582 &mut frames,
583 &mut budget,
584 transaction_ids_frame(request_id, truncate_to_inventory_cap(ids)?)?,
585 )?;
586 }
587 Response::Pong(_) => {
588 push_response_frame(
589 &mut frames,
590 &mut budget,
591 id_only_frame(MSG_RESPONSE_PONG, request_id),
592 )?;
593 }
594 Response::Nil => {
595 push_response_frame(
596 &mut frames,
597 &mut budget,
598 id_only_frame(MSG_RESPONSE_NIL, request_id),
599 )?;
600 }
601 response => return Err(LegacyGossipError::UnexpectedResponse(response.command())),
602 }
603 Ok(frames)
604 }
605
606 pub(super) fn decode_response(
607 request_id: u64,
608 request_kind: LegacyRequestKind,
609 frames: Vec<Frame>,
610 requested_block_hashes: Option<&HashSet<block::Hash>>,
611 ) -> Result<Response, LegacyGossipError> {
612 let mut blocks = Vec::new();
613 let mut transactions = Vec::new();
614 let mut block_hashes = Vec::new();
615 let mut block_headers = Vec::new();
616 let mut transaction_ids = Vec::new();
617 let mut saw_pong = false;
618 let mut saw_nil = false;
619 let mut reassembler = ResponseReassembler::new(request_id);
620
621 for frame in frames {
622 if frame.flags != 0 {
623 return Err(LegacyGossipError::UnsupportedFlags(frame.flags));
624 }
625
626 match frame.message_type {
627 MSG_RESPONSE_BLOCK => {
628 if request_kind != LegacyRequestKind::Blocks {
629 return Err(LegacyGossipError::UnexpectedResponse("Blocks"));
630 }
631 if let Some(bytes) = reassembler.accept(&frame.payload)? {
632 let block = Arc::new(Block::zcash_deserialize(&mut Cursor::new(
633 bytes.as_slice(),
634 ))?);
635 if let Some(requested) = requested_block_hashes {
641 let hash = block.hash();
642 if !requested.contains(&hash) {
643 return Err(LegacyGossipError::UnsolicitedBlock(hash));
644 }
645 }
646 blocks.push(InventoryResponse::Available((block, None)));
647 }
648 }
649 MSG_RESPONSE_TRANSACTION => {
650 if request_kind != LegacyRequestKind::Transactions {
651 return Err(LegacyGossipError::UnexpectedResponse("Transactions"));
652 }
653 if let Some(bytes) = reassembler.accept(&frame.payload)? {
654 let transaction =
655 Transaction::zcash_deserialize(&mut Cursor::new(bytes.as_slice()))?;
656 transactions.push(InventoryResponse::Available((
657 UnminedTx::from(transaction),
658 None,
659 )));
660 }
661 }
662 MSG_RESPONSE_MISSING_BLOCKS => {
663 if request_kind != LegacyRequestKind::Blocks {
664 return Err(LegacyGossipError::UnexpectedResponse("MissingBlocks"));
665 }
666 reassembler.reject_if_active()?;
667 for hash in decode_hashes_response(request_id, frame.payload)? {
668 if let Some(requested) = requested_block_hashes {
670 if !requested.contains(&hash) {
671 return Err(LegacyGossipError::UnsolicitedBlock(hash));
672 }
673 }
674 blocks.push(InventoryResponse::Missing(hash));
675 }
676 }
677 MSG_RESPONSE_MISSING_TRANSACTIONS => {
678 if request_kind != LegacyRequestKind::Transactions {
679 return Err(LegacyGossipError::UnexpectedResponse("MissingTransactions"));
680 }
681 reassembler.reject_if_active()?;
682 for id in decode_tx_ids_response(request_id, frame.payload)? {
683 transactions.push(InventoryResponse::Missing(id));
684 }
685 }
686 MSG_RESPONSE_BLOCK_HASHES => {
687 if request_kind != LegacyRequestKind::FindBlocks {
688 return Err(LegacyGossipError::UnexpectedResponse("BlockHashes"));
689 }
690 reassembler.reject_if_active()?;
691 block_hashes.extend(decode_hashes_response(request_id, frame.payload)?);
692 }
693 MSG_RESPONSE_BLOCK_HEADERS => {
694 if request_kind != LegacyRequestKind::FindHeaders {
695 return Err(LegacyGossipError::UnexpectedResponse("BlockHeaders"));
696 }
697 reassembler.reject_if_active()?;
698 block_headers.extend(decode_block_headers(request_id, frame.payload)?);
699 }
700 MSG_RESPONSE_TRANSACTION_IDS => {
701 if request_kind != LegacyRequestKind::MempoolTransactionIds {
702 return Err(LegacyGossipError::UnexpectedResponse("TransactionIds"));
703 }
704 reassembler.reject_if_active()?;
705 transaction_ids.extend(decode_tx_ids_response(request_id, frame.payload)?);
706 }
707 MSG_RESPONSE_PONG => {
708 if request_kind != LegacyRequestKind::Ping {
709 return Err(LegacyGossipError::UnexpectedResponse("Pong"));
710 }
711 reassembler.reject_if_active()?;
712 decode_id_only_response(request_id, frame.payload)?;
713 saw_pong = true;
714 }
715 MSG_RESPONSE_NIL => {
716 match request_kind {
727 LegacyRequestKind::FindBlocks
728 | LegacyRequestKind::FindHeaders
729 | LegacyRequestKind::MempoolTransactionIds
730 | LegacyRequestKind::PushTransaction => {}
731 LegacyRequestKind::Blocks
732 | LegacyRequestKind::Transactions
733 | LegacyRequestKind::Ping => {
734 return Err(LegacyGossipError::UnexpectedResponse("Nil"));
735 }
736 }
737 reassembler.reject_if_active()?;
738 decode_id_only_response(request_id, frame.payload)?;
739 saw_nil = true;
740 }
741 message_type => return Err(LegacyGossipError::UnknownMessageType(message_type)),
742 }
743 }
744
745 reassembler.finish()?;
746
747 match request_kind {
748 LegacyRequestKind::Blocks => Ok(Response::Blocks(blocks)),
749 LegacyRequestKind::Transactions => Ok(Response::Transactions(transactions)),
750 LegacyRequestKind::FindBlocks => Ok(Response::BlockHashes(block_hashes)),
751 LegacyRequestKind::FindHeaders => Ok(Response::BlockHeaders(block_headers)),
752 LegacyRequestKind::MempoolTransactionIds => {
753 Ok(Response::TransactionIds(transaction_ids))
754 }
755 LegacyRequestKind::Ping if saw_pong => Ok(Response::Pong(Duration::ZERO)),
756 LegacyRequestKind::PushTransaction if saw_nil => Ok(Response::Nil),
757 LegacyRequestKind::Ping | LegacyRequestKind::PushTransaction => {
758 Err(LegacyGossipError::MissingResponse(request_kind.command()))
759 }
760 }
761 }
762}
763
764fn collect_outbound_tx_ids(
765 ids: impl IntoIterator<Item = UnminedTxId>,
766) -> Result<Vec<UnminedTxId>, LegacyGossipError> {
767 let max_tx_inv_in_message = usize::try_from(MAX_TX_INV_IN_SENT_MESSAGE)?;
768 let ids: Vec<_> = ids.into_iter().take(max_tx_inv_in_message).collect();
769 if ids.is_empty() {
770 return Err(LegacyGossipError::EmptyTransactionAdvertisement);
771 }
772 Ok(ids)
773}
774
775fn truncate_to_inventory_cap<T>(
776 items: impl IntoIterator<Item = T>,
777) -> Result<Vec<T>, LegacyGossipError> {
778 let max = usize::try_from(MAX_TX_INV_IN_SENT_MESSAGE)?;
780 let mut collected = Vec::new();
781 let mut dropped = 0usize;
782
783 for item in items {
784 if collected.len() < max {
785 collected.push(item);
786 } else {
787 dropped = dropped.saturating_add(1);
788 }
789 }
790
791 if dropped > 0 {
792 debug!(
793 dropped,
794 max, "legacy inventory request exceeded item cap; dropping extras"
795 );
796 }
797
798 Ok(collected)
799}
800
801fn write_hash_list(out: &mut Vec<u8>, hashes: &[block::Hash]) -> Result<(), LegacyGossipError> {
802 ensure_inventory_count(hashes.len())?;
803 CompactSizeMessage::try_from(hashes.len())?.zcash_serialize(&mut *out)?;
804 for hash in hashes {
805 hash.zcash_serialize(&mut *out)?;
806 }
807 Ok(())
808}
809
810fn read_hash_list(reader: &mut Cursor<&[u8]>) -> Result<Vec<block::Hash>, LegacyGossipError> {
811 let count = bounded_inventory_count(reader)?;
812 let mut hashes = Vec::with_capacity(count);
813 for _ in 0..count {
814 hashes.push(block::Hash::zcash_deserialize(&mut *reader)?);
815 }
816 Ok(hashes)
817}
818
819fn write_block_locator(
820 out: &mut Vec<u8>,
821 known_blocks: &[block::Hash],
822 stop: Option<block::Hash>,
823) -> Result<(), LegacyGossipError> {
824 ensure_block_locator_count(known_blocks.len())?;
825 CompactSizeMessage::try_from(known_blocks.len())?.zcash_serialize(&mut *out)?;
826 for hash in known_blocks {
827 hash.zcash_serialize(&mut *out)?;
828 }
829 stop.unwrap_or(NO_STOP_HASH).zcash_serialize(&mut *out)?;
830 Ok(())
831}
832
833fn read_block_locator(
834 reader: &mut Cursor<&[u8]>,
835) -> Result<(Vec<block::Hash>, Option<block::Hash>), LegacyGossipError> {
836 let count = bounded_block_locator_count(reader)?;
837 let mut known_blocks = Vec::with_capacity(count);
838 for _ in 0..count {
839 known_blocks.push(block::Hash::zcash_deserialize(&mut *reader)?);
840 }
841 let stop_hash = block::Hash::zcash_deserialize(&mut *reader)?;
842 let stop = (stop_hash != NO_STOP_HASH).then_some(stop_hash);
844 Ok((known_blocks, stop))
845}
846
847fn write_tx_id_list(out: &mut Vec<u8>, ids: &[UnminedTxId]) -> Result<(), LegacyGossipError> {
848 ensure_inventory_count(ids.len())?;
849 CompactSizeMessage::try_from(ids.len())?.zcash_serialize(&mut *out)?;
850 for id in ids {
851 InventoryHash::from(id).zcash_serialize(&mut *out)?;
852 }
853 Ok(())
854}
855
856fn read_tx_id_list(reader: &mut Cursor<&[u8]>) -> Result<Vec<UnminedTxId>, LegacyGossipError> {
857 let count = bounded_inventory_count(reader)?;
858 let mut ids = Vec::with_capacity(count);
859 for _ in 0..count {
860 let inv = InventoryHash::zcash_deserialize(&mut *reader)?;
861 let Some(id) = inv.unmined_tx_id() else {
862 return Err(LegacyGossipError::NonTransactionInventory);
863 };
864 ids.push(id);
865 }
866 Ok(ids)
867}
868
869fn bounded_inventory_count(reader: &mut Cursor<&[u8]>) -> Result<usize, LegacyGossipError> {
870 let count = usize::from(CompactSizeMessage::zcash_deserialize(reader)?);
871 ensure_inventory_count(count)?;
872 Ok(count)
873}
874
875fn ensure_inventory_count(count: usize) -> Result<(), LegacyGossipError> {
876 let max = usize::try_from(MAX_TX_INV_IN_SENT_MESSAGE)?;
877 if count > max {
878 return Err(LegacyGossipError::TooManyInventoryItems(count));
879 }
880 Ok(())
881}
882
883fn bounded_block_locator_count(reader: &mut Cursor<&[u8]>) -> Result<usize, LegacyGossipError> {
884 let count = usize::from(CompactSizeMessage::zcash_deserialize(reader)?);
885 ensure_block_locator_count(count)?;
886 Ok(count)
887}
888
889fn ensure_block_locator_count(count: usize) -> Result<(), LegacyGossipError> {
890 let max = usize::try_from(MAX_BLOCK_LOCATOR_LENGTH)?;
891 if count > max {
892 return Err(LegacyGossipError::TooManyBlockLocatorHashes(count));
893 }
894 Ok(())
895}
896
897fn write_header_list(
898 out: &mut Vec<u8>,
899 headers: &[block::CountedHeader],
900) -> Result<(), LegacyGossipError> {
901 ensure_header_count(headers.len())?;
902 CompactSizeMessage::try_from(headers.len())?.zcash_serialize(&mut *out)?;
903 for header in headers {
904 header.zcash_serialize(&mut *out)?;
905 }
906 Ok(())
907}
908
909fn read_header_list(
910 reader: &mut Cursor<&[u8]>,
911) -> Result<Vec<block::CountedHeader>, LegacyGossipError> {
912 let count = bounded_header_count(reader)?;
913 let mut headers = Vec::with_capacity(count);
914 for _ in 0..count {
915 headers.push(block::CountedHeader::zcash_deserialize(&mut *reader)?);
916 }
917 Ok(headers)
918}
919
920fn bounded_header_count(reader: &mut Cursor<&[u8]>) -> Result<usize, LegacyGossipError> {
921 let count = usize::from(CompactSizeMessage::zcash_deserialize(reader)?);
922 ensure_header_count(count)?;
923 Ok(count)
924}
925
926fn ensure_header_count(count: usize) -> Result<(), LegacyGossipError> {
927 if count > MAX_HEADERS_PER_MESSAGE {
928 return Err(LegacyGossipError::TooManyHeaders(count));
929 }
930 Ok(())
931}
932
933#[derive(Default)]
938struct ResponseEncodeBudget {
939 bytes: usize,
940}
941
942impl ResponseEncodeBudget {
943 fn account(&mut self, payload_len: usize) -> Result<(), LegacyGossipError> {
947 self.bytes = self.bytes.saturating_add(payload_len);
948 if self.bytes > LEGACY_RESPONSE_MAX_AGGREGATE_BYTES {
949 return Err(LegacyGossipError::ResponseAggregateBudget(self.bytes));
950 }
951 Ok(())
952 }
953}
954
955fn push_response_frame(
958 frames: &mut Vec<Frame>,
959 budget: &mut ResponseEncodeBudget,
960 frame: Frame,
961) -> Result<(), LegacyGossipError> {
962 budget.account(frame.payload.len())?;
963 frames.push(frame);
964 Ok(())
965}
966
967fn push_chunked_response(
968 frames: &mut Vec<Frame>,
969 budget: &mut ResponseEncodeBudget,
970 message_type: u16,
971 request_id: u64,
972 max_frame_bytes: u32,
973 max_message_bytes: u32,
974 bytes: Vec<u8>,
975) -> Result<(), LegacyGossipError> {
976 if bytes.len() > MAX_PROTOCOL_MESSAGE_LEN {
977 return Err(LegacyGossipError::OversizedResponse(bytes.len()));
978 }
979
980 let max_payload_bytes = usize::try_from(max_frame_bytes)?
987 .saturating_sub(FRAME_HEADER_BYTES)
988 .min(usize::try_from(max_message_bytes)?);
989 let max_chunk_bytes = max_payload_bytes
990 .checked_sub(RESPONSE_CHUNK_HEADER_BYTES)
991 .ok_or(LegacyGossipError::OversizedResponse(bytes.len()))?;
992 if max_chunk_bytes == 0 {
993 return Err(LegacyGossipError::OversizedResponse(bytes.len()));
994 }
995 let chunk_bytes = LEGACY_RESPONSE_CHUNK_BYTES.min(max_chunk_bytes);
996
997 debug_assert!(!bytes.is_empty());
998 let chunks = bytes.chunks(chunk_bytes).collect::<Vec<_>>();
999
1000 for (index, chunk) in chunks.iter().enumerate() {
1001 let mut payload = Vec::with_capacity(RESPONSE_CHUNK_HEADER_BYTES + chunk.len());
1002 payload.extend_from_slice(&request_id.to_le_bytes());
1005 payload.push(u8::from(index + 1 == chunks.len()));
1006 payload.extend_from_slice(chunk);
1007 budget.account(payload.len())?;
1011 frames.push(Frame {
1012 message_type,
1013 flags: 0,
1014 payload,
1015 });
1016 }
1017 Ok(())
1018}
1019
1020struct ResponseReassembler {
1021 expected_id: u64,
1022 active: bool,
1023 buffer: Vec<u8>,
1024}
1025
1026impl ResponseReassembler {
1027 fn new(expected_id: u64) -> Self {
1028 Self {
1029 expected_id,
1030 active: false,
1031 buffer: Vec::new(),
1032 }
1033 }
1034
1035 fn accept(&mut self, payload: &[u8]) -> Result<Option<Vec<u8>>, LegacyGossipError> {
1036 let (response_id, is_last, bytes) = decode_response_chunk_header(payload)?;
1037 if response_id != self.expected_id {
1038 return Err(LegacyGossipError::WrongRequestId {
1039 expected: self.expected_id,
1040 actual: response_id,
1041 });
1042 }
1043
1044 let new_len = self
1045 .buffer
1046 .len()
1047 .checked_add(bytes.len())
1048 .ok_or(LegacyGossipError::OversizedResponse(usize::MAX))?;
1049 if new_len > MAX_PROTOCOL_MESSAGE_LEN {
1050 return Err(LegacyGossipError::OversizedResponse(new_len));
1051 }
1052 self.active = true;
1053 self.buffer.extend_from_slice(bytes);
1054
1055 if is_last {
1056 self.active = false;
1057 Ok(Some(std::mem::take(&mut self.buffer)))
1058 } else {
1059 Ok(None)
1060 }
1061 }
1062
1063 fn reject_if_active(&self) -> Result<(), LegacyGossipError> {
1064 if self.active {
1065 Err(LegacyGossipError::IncompleteResponseChunk)
1066 } else {
1067 Ok(())
1068 }
1069 }
1070
1071 fn finish(self) -> Result<(), LegacyGossipError> {
1072 if self.active {
1073 Err(LegacyGossipError::IncompleteResponseChunk)
1074 } else {
1075 Ok(())
1076 }
1077 }
1078}
1079
1080fn decode_response_chunk_header(payload: &[u8]) -> Result<(u64, bool, &[u8]), LegacyGossipError> {
1081 if payload.len() < RESPONSE_CHUNK_HEADER_BYTES {
1082 return Err(LegacyGossipError::TruncatedResponse);
1083 }
1084 let (request_id, payload) = read_request_id(payload)?;
1085 Ok((request_id, payload[0] != 0, &payload[1..]))
1086}
1087
1088fn missing_blocks_frame(
1089 request_id: u64,
1090 hashes: Vec<block::Hash>,
1091) -> Result<Frame, LegacyGossipError> {
1092 let mut payload = Vec::new();
1093 payload.extend_from_slice(&request_id.to_le_bytes());
1095 write_hash_list(&mut payload, &hashes)?;
1096 Ok(Frame {
1097 message_type: MSG_RESPONSE_MISSING_BLOCKS,
1098 flags: 0,
1099 payload,
1100 })
1101}
1102
1103fn missing_transactions_frame(
1104 request_id: u64,
1105 ids: Vec<UnminedTxId>,
1106) -> Result<Frame, LegacyGossipError> {
1107 let mut payload = Vec::new();
1108 payload.extend_from_slice(&request_id.to_le_bytes());
1109 write_tx_id_list(&mut payload, &ids)?;
1110 Ok(Frame {
1111 message_type: MSG_RESPONSE_MISSING_TRANSACTIONS,
1112 flags: 0,
1113 payload,
1114 })
1115}
1116
1117fn block_hashes_frame(
1118 request_id: u64,
1119 hashes: Vec<block::Hash>,
1120) -> Result<Frame, LegacyGossipError> {
1121 let mut payload = Vec::new();
1122 payload.extend_from_slice(&request_id.to_le_bytes());
1123 write_hash_list(&mut payload, &hashes)?;
1124 Ok(Frame {
1125 message_type: MSG_RESPONSE_BLOCK_HASHES,
1126 flags: 0,
1127 payload,
1128 })
1129}
1130
1131fn block_headers_frame(
1132 request_id: u64,
1133 headers: Vec<block::CountedHeader>,
1134) -> Result<Frame, LegacyGossipError> {
1135 let mut payload = Vec::new();
1136 payload.extend_from_slice(&request_id.to_le_bytes());
1137 write_header_list(&mut payload, &headers)?;
1138 Ok(Frame {
1139 message_type: MSG_RESPONSE_BLOCK_HEADERS,
1140 flags: 0,
1141 payload,
1142 })
1143}
1144
1145fn transaction_ids_frame(
1146 request_id: u64,
1147 ids: Vec<UnminedTxId>,
1148) -> Result<Frame, LegacyGossipError> {
1149 let mut payload = Vec::new();
1150 payload.extend_from_slice(&request_id.to_le_bytes());
1151 write_tx_id_list(&mut payload, &ids)?;
1152 Ok(Frame {
1153 message_type: MSG_RESPONSE_TRANSACTION_IDS,
1154 flags: 0,
1155 payload,
1156 })
1157}
1158
1159fn id_only_frame(message_type: u16, request_id: u64) -> Frame {
1160 Frame {
1161 message_type,
1162 flags: 0,
1163 payload: request_id.to_le_bytes().to_vec(),
1164 }
1165}
1166
1167fn read_request_id(payload: &[u8]) -> Result<(u64, &[u8]), LegacyGossipError> {
1168 if payload.len() < REQUEST_ID_BYTES {
1169 return Err(LegacyGossipError::TruncatedResponse);
1170 }
1171 let mut id_bytes = [0; REQUEST_ID_BYTES];
1172 id_bytes.copy_from_slice(&payload[..REQUEST_ID_BYTES]);
1173 Ok((u64::from_le_bytes(id_bytes), &payload[REQUEST_ID_BYTES..]))
1174}
1175
1176fn verify_response_id(request_id: u64, payload: &[u8]) -> Result<&[u8], LegacyGossipError> {
1177 let (response_id, payload) = read_request_id(payload)?;
1178 if response_id != request_id {
1179 return Err(LegacyGossipError::WrongRequestId {
1180 expected: request_id,
1181 actual: response_id,
1182 });
1183 }
1184 Ok(payload)
1185}
1186
1187fn decode_hashes_response(
1188 request_id: u64,
1189 payload: Vec<u8>,
1190) -> Result<Vec<block::Hash>, LegacyGossipError> {
1191 let payload = verify_response_id(request_id, &payload)?;
1192 let mut reader = Cursor::new(payload);
1193 let hashes = read_hash_list(&mut reader)?;
1194 reject_trailing(&reader)?;
1195 Ok(hashes)
1196}
1197
1198fn decode_tx_ids_response(
1199 request_id: u64,
1200 payload: Vec<u8>,
1201) -> Result<Vec<UnminedTxId>, LegacyGossipError> {
1202 let payload = verify_response_id(request_id, &payload)?;
1203 let mut reader = Cursor::new(payload);
1204 let ids = read_tx_id_list(&mut reader)?;
1205 reject_trailing(&reader)?;
1206 Ok(ids)
1207}
1208
1209fn decode_block_headers(
1210 request_id: u64,
1211 payload: Vec<u8>,
1212) -> Result<Vec<block::CountedHeader>, LegacyGossipError> {
1213 let payload = verify_response_id(request_id, &payload)?;
1214 let mut reader = Cursor::new(payload);
1215 let headers = read_header_list(&mut reader)?;
1216 reject_trailing(&reader)?;
1217 Ok(headers)
1218}
1219
1220fn decode_id_only_response(request_id: u64, payload: Vec<u8>) -> Result<(), LegacyGossipError> {
1221 let payload = verify_response_id(request_id, &payload)?;
1222 if !payload.is_empty() {
1223 return Err(LegacyGossipError::TrailingBytes);
1224 }
1225 Ok(())
1226}
1227
1228fn outbound_tx_id_slice(ids: &[UnminedTxId]) -> Result<&[UnminedTxId], LegacyGossipError> {
1229 let max_tx_inv_in_message = usize::try_from(MAX_TX_INV_IN_SENT_MESSAGE)?;
1230 let ids = &ids[..ids.len().min(max_tx_inv_in_message)];
1231 if ids.is_empty() {
1232 return Err(LegacyGossipError::EmptyTransactionAdvertisement);
1233 }
1234 Ok(ids)
1235}
1236
1237fn reject_trailing(reader: &Cursor<&[u8]>) -> Result<(), LegacyGossipError> {
1238 let len = u64::try_from(reader.get_ref().len())?;
1239 if reader.position() != len {
1240 return Err(LegacyGossipError::TrailingBytes);
1241 }
1242 Ok(())
1243}
1244
1245#[derive(Clone, Debug)]
1247pub struct ZakuraGossipBroadcast {
1248 first_seen: FirstSeenCache,
1249 outbound: LegacyGossipOutbound,
1250}
1251
1252impl ZakuraGossipBroadcast {
1253 pub fn new(supervisor: ZakuraSupervisorHandle) -> Self {
1255 let first_seen = first_seen_for_supervisor(&supervisor);
1256 let outbound = outbound_for_supervisor(&supervisor);
1257 Self {
1258 first_seen,
1259 outbound,
1260 }
1261 }
1262
1263 async fn broadcast(
1264 &self,
1265 frame: LegacyGossipFrame,
1266 exclude: Option<&ZakuraPeerId>,
1267 ) -> Result<(), BoxError> {
1268 self.outbound.remember_latest_block(&frame);
1269 self.outbound.send_to_peers(frame, exclude).await
1270 }
1271
1272 async fn record_first_seen(&self, frame: &LegacyGossipFrame) -> Option<LegacyGossipFrame> {
1273 self.first_seen.record_first_seen(frame).await
1274 }
1275
1276 async fn unseen(&self, frame: &LegacyGossipFrame) -> Option<LegacyGossipFrame> {
1277 self.first_seen.unseen(frame).await
1278 }
1279
1280 async fn mark_seen(&self, frame: &LegacyGossipFrame) {
1281 self.first_seen.record_seen(frame).await;
1282 }
1283}
1284
1285#[derive(Clone, Debug, Default)]
1286struct LegacyGossipOutbound {
1287 sessions: Arc<StdMutex<HashMap<ZakuraPeerId, LegacyGossipPeerSession>>>,
1288 latest_block: Arc<StdMutex<Option<block::Hash>>>,
1289}
1290
1291impl LegacyGossipOutbound {
1292 fn insert(&self, session: LegacyGossipPeerSession) -> bool {
1293 let mut sessions = self
1294 .sessions
1295 .lock()
1296 .expect("legacy gossip outbound mutex is never poisoned");
1297 if sessions
1298 .get(session.peer_id())
1299 .is_some_and(|active| active.conn_id() > session.conn_id())
1300 {
1301 return false;
1302 }
1303 sessions.insert(session.peer_id().clone(), session);
1304 true
1305 }
1306
1307 fn remove(&self, peer: &ZakuraPeerId, conn_id: ZakuraConnId) {
1308 let mut sessions = self
1309 .sessions
1310 .lock()
1311 .expect("legacy gossip outbound mutex is never poisoned");
1312 if sessions
1313 .get(peer)
1314 .is_some_and(|session| session.conn_id() == conn_id)
1315 {
1316 sessions.remove(peer);
1317 }
1318 }
1319
1320 #[cfg(test)]
1321 fn contains(&self, peer: &ZakuraPeerId) -> bool {
1322 self.sessions
1323 .lock()
1324 .expect("legacy gossip outbound mutex is never poisoned")
1325 .contains_key(peer)
1326 }
1327
1328 fn remember_latest_block(&self, frame: &LegacyGossipFrame) {
1329 if let LegacyGossipFrame::AdvertiseBlock(hash) = frame {
1330 *self
1331 .latest_block
1332 .lock()
1333 .expect("legacy gossip latest-block mutex is never poisoned") = Some(*hash);
1334 }
1335 }
1336
1337 async fn replay_latest_block_to_peer(
1338 &self,
1339 session: LegacyGossipPeerSession,
1340 ) -> Result<(), BoxError> {
1341 let Some(hash) = *self
1342 .latest_block
1343 .lock()
1344 .expect("legacy gossip latest-block mutex is never poisoned")
1345 else {
1346 return Ok(());
1347 };
1348
1349 session.try_send_advertise_block(hash).map_err(Into::into)
1350 }
1351
1352 async fn send_to_peers(
1353 &self,
1354 frame: LegacyGossipFrame,
1355 exclude: Option<&ZakuraPeerId>,
1356 ) -> Result<(), BoxError> {
1357 let sessions: Vec<_> = {
1358 let sessions = self
1359 .sessions
1360 .lock()
1361 .expect("legacy gossip outbound mutex is never poisoned");
1362 sessions
1363 .iter()
1364 .filter(|(peer_id, _)| !exclude.is_some_and(|exclude| exclude == *peer_id))
1365 .map(|(_peer_id, session)| session.clone())
1366 .collect()
1367 };
1368
1369 send_to_sessions(sessions, frame)
1370 }
1371}
1372
1373#[cfg(test)]
1374fn legacy_gossip_recv_loop_panic_target() -> &'static StdMutex<Option<ZakuraPeerId>> {
1375 static TARGET: OnceLock<StdMutex<Option<ZakuraPeerId>>> = OnceLock::new();
1376 TARGET.get_or_init(Default::default)
1377}
1378
1379#[cfg(test)]
1380fn arm_legacy_gossip_recv_loop_panic(peer: ZakuraPeerId) {
1381 *legacy_gossip_recv_loop_panic_target()
1382 .lock()
1383 .expect("legacy gossip recv-loop panic target mutex is never poisoned") = Some(peer);
1384}
1385
1386#[cfg(test)]
1387fn should_panic_legacy_gossip_recv_loop(peer: &ZakuraPeerId) -> bool {
1388 let mut target = legacy_gossip_recv_loop_panic_target()
1389 .lock()
1390 .expect("legacy gossip recv-loop panic target mutex is never poisoned");
1391 if target.as_ref() == Some(peer) {
1392 *target = None;
1393 true
1394 } else {
1395 false
1396 }
1397}
1398
1399#[derive(Clone, Debug)]
1401pub struct LegacyGossipPeerSession {
1402 peer_id: ZakuraPeerId,
1403 conn_id: ZakuraConnId,
1404 send: FramedSend,
1405}
1406
1407impl LegacyGossipPeerSession {
1408 fn new(peer_id: ZakuraPeerId, conn_id: ZakuraConnId, send: FramedSend) -> Self {
1409 Self {
1410 peer_id,
1411 conn_id,
1412 send,
1413 }
1414 }
1415
1416 pub fn peer_id(&self) -> &ZakuraPeerId {
1418 &self.peer_id
1419 }
1420
1421 fn conn_id(&self) -> ZakuraConnId {
1422 self.conn_id
1423 }
1424
1425 pub fn try_send_advertise_block(&self, hash: block::Hash) -> Result<(), OrderedSendError> {
1427 self.try_send_gossip_frame(LegacyGossipFrame::AdvertiseBlock(hash))
1428 }
1429
1430 pub fn try_send_advertise_transaction_ids(
1432 &self,
1433 ids: Vec<UnminedTxId>,
1434 ) -> Result<(), OrderedSendError> {
1435 self.try_send_gossip_frame(LegacyGossipFrame::AdvertiseTransactionIds(ids))
1436 }
1437
1438 fn try_send_gossip_frame(&self, frame: LegacyGossipFrame) -> Result<(), OrderedSendError> {
1439 let frame = frame
1440 .encode_frame()
1441 .map_err(|error| OrderedSendError::Encode(Box::new(error)))?;
1442 match self.send.try_send(frame) {
1443 Ok(()) => Ok(()),
1444 Err(mpsc::error::TrySendError::Full(_frame)) => Err(OrderedSendError::Full),
1445 Err(mpsc::error::TrySendError::Closed(_frame)) => Err(OrderedSendError::Closed),
1446 }
1447 }
1448}
1449
1450static LEGACY_GOSSIP_OUTBOUND_BY_SUPERVISOR: OnceLock<
1451 std::sync::Mutex<HashMap<u64, LegacyGossipOutbound>>,
1452> = OnceLock::new();
1453
1454fn outbound_for_supervisor(supervisor: &ZakuraSupervisorHandle) -> LegacyGossipOutbound {
1455 let registry = LEGACY_GOSSIP_OUTBOUND_BY_SUPERVISOR.get_or_init(Default::default);
1456 let mut registry = registry
1457 .lock()
1458 .expect("legacy gossip outbound registry mutex is never poisoned");
1459 registry.entry(supervisor.id()).or_default().clone()
1460}
1461
1462fn first_seen_for_supervisor(supervisor: &ZakuraSupervisorHandle) -> FirstSeenCache {
1463 let registry = FIRST_SEEN_BY_SUPERVISOR.get_or_init(Default::default);
1464 let mut registry = registry
1465 .lock()
1466 .expect("legacy gossip first-seen registry mutex is never poisoned");
1467 registry
1468 .entry(supervisor.id())
1469 .or_insert_with(|| FirstSeenCache::new(DEFAULT_FIRST_SEEN_CAPACITY, DEFAULT_FIRST_SEEN_TTL))
1470 .clone()
1471}
1472
1473fn send_to_sessions(
1474 sessions: Vec<LegacyGossipPeerSession>,
1475 frame: LegacyGossipFrame,
1476) -> Result<(), BoxError> {
1477 let mut first_error = None;
1478 for session in sessions {
1479 let result = match &frame {
1480 LegacyGossipFrame::AdvertiseBlock(hash) => session.try_send_advertise_block(*hash),
1481 LegacyGossipFrame::AdvertiseTransactionIds(ids) => {
1482 session.try_send_advertise_transaction_ids(ids.clone())
1483 }
1484 };
1485
1486 if let Err(error) = result {
1487 debug!(
1488 peer = ?session.peer_id(),
1489 ?error,
1490 "failed to queue Zakura legacy gossip frame"
1491 );
1492 if first_error.is_none() {
1493 first_error = Some(error.into());
1494 }
1495 }
1496 }
1497
1498 if let Some(error) = first_error {
1499 return Err(error);
1500 }
1501 Ok(())
1502}
1503
1504#[derive(Clone, Debug)]
1506pub struct LegacyGossipAdapter {
1507 broadcast: ZakuraGossipBroadcast,
1508}
1509
1510impl LegacyGossipAdapter {
1511 pub fn new(supervisor: ZakuraSupervisorHandle) -> Self {
1513 Self {
1514 broadcast: ZakuraGossipBroadcast::new(supervisor),
1515 }
1516 }
1517}
1518
1519impl Service<Request> for LegacyGossipAdapter {
1520 type Response = Response;
1521 type Error = BoxError;
1522 type Future = Pin<Box<dyn Future<Output = Result<Response, BoxError>> + Send + 'static>>;
1523
1524 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1525 Poll::Ready(Ok(()))
1526 }
1527
1528 fn call(&mut self, request: Request) -> Self::Future {
1529 let broadcast = self.broadcast.clone();
1530 Box::pin(async move {
1531 let frame = LegacyGossipFrame::from_request(request)?;
1532 let Some(frame) = broadcast.record_first_seen(&frame).await else {
1533 return Ok(Response::Nil);
1534 };
1535 broadcast.broadcast(frame, None).await?;
1536 Ok(Response::Nil)
1537 })
1538 }
1539}
1540
1541#[derive(Clone, Debug)]
1547pub struct LegacyRequestAdapter {
1548 client: ZakuraRequestClient,
1549}
1550
1551impl LegacyRequestAdapter {
1552 pub fn new(supervisor: ZakuraSupervisorHandle) -> Self {
1554 Self {
1555 client: ZakuraRequestClient::new(supervisor),
1556 }
1557 }
1558
1559 pub fn new_with_trace(supervisor: ZakuraSupervisorHandle, trace: ZakuraTrace) -> Self {
1561 Self {
1562 client: ZakuraRequestClient::new_with_trace(supervisor, trace),
1563 }
1564 }
1565
1566 #[cfg(test)]
1567 fn new_with_timeout(supervisor: ZakuraSupervisorHandle, request_timeout: Duration) -> Self {
1568 Self {
1569 client: ZakuraRequestClient::new_with_timeout(supervisor, request_timeout),
1570 }
1571 }
1572
1573 pub async fn request_from_source(
1575 &self,
1576 request: Request,
1577 source: Option<PeerSource>,
1578 ) -> Result<Response, BoxError> {
1579 let frame = LegacyRequestFrame::from_request(request)?;
1580 self.client.request(frame, source, false).await
1581 }
1582}
1583
1584impl Service<Request> for LegacyRequestAdapter {
1585 type Response = Response;
1586 type Error = BoxError;
1587 type Future = Pin<Box<dyn Future<Output = Result<Response, BoxError>> + Send + 'static>>;
1588
1589 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1590 Poll::Ready(Ok(()))
1591 }
1592
1593 fn call(&mut self, request: Request) -> Self::Future {
1594 let client = self.client.clone();
1595 Box::pin(async move {
1596 let source = request.inventory_source();
1597 let retry_source_missing = source.is_some();
1598 let frame = LegacyRequestFrame::from_request(request)?;
1599 client.request(frame, source, retry_source_missing).await
1600 })
1601 }
1602}
1603
1604#[derive(Copy, Clone)]
1606enum DualStackRoute {
1607 Advertise,
1609 LegacyFirstThenZakura,
1617 Passthrough,
1620}
1621
1622#[derive(Clone)]
1646pub(crate) struct ZakuraDualStackService<L> {
1647 legacy: L,
1648 gossip: LegacyGossipAdapter,
1649 request: LegacyRequestAdapter,
1650 legacy_enabled: bool,
1651}
1652
1653impl<L> ZakuraDualStackService<L> {
1654 #[cfg(test)]
1659 pub(crate) fn new(legacy: L, supervisor: ZakuraSupervisorHandle, legacy_enabled: bool) -> Self {
1660 Self {
1661 legacy,
1662 gossip: LegacyGossipAdapter::new(supervisor.clone()),
1663 request: LegacyRequestAdapter::new(supervisor),
1664 legacy_enabled,
1665 }
1666 }
1667
1668 pub(crate) fn new_with_trace(
1670 legacy: L,
1671 supervisor: ZakuraSupervisorHandle,
1672 legacy_enabled: bool,
1673 trace: ZakuraTrace,
1674 ) -> Self {
1675 Self {
1676 legacy,
1677 gossip: LegacyGossipAdapter::new(supervisor.clone()),
1678 request: LegacyRequestAdapter::new_with_trace(supervisor, trace),
1679 legacy_enabled,
1680 }
1681 }
1682}
1683
1684impl<L> Service<Request> for ZakuraDualStackService<L>
1685where
1686 L: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
1687 L::Future: Send + 'static,
1688{
1689 type Response = Response;
1690 type Error = BoxError;
1691 type Future = Pin<Box<dyn Future<Output = Result<Response, BoxError>> + Send + 'static>>;
1692
1693 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1694 std::task::ready!(self.gossip.poll_ready(cx))?;
1698 std::task::ready!(self.request.poll_ready(cx))?;
1699 Poll::Ready(Ok(()))
1700 }
1701
1702 fn call(&mut self, request: Request) -> Self::Future {
1703 let route = match &request {
1704 Request::AdvertiseBlock(..)
1705 | Request::AdvertiseBlockToAll(..)
1706 | Request::AdvertiseTransactionIds(..) => DualStackRoute::Advertise,
1707 Request::BlocksByHash(..)
1708 | Request::BlocksByHashFrom { .. }
1709 | Request::TransactionsById(..)
1710 | Request::TransactionsByIdFrom { .. }
1711 | Request::FindBlocks { .. }
1712 | Request::FindHeaders { .. }
1713 | Request::MempoolTransactionIds
1714 | Request::PushTransaction(..) => DualStackRoute::LegacyFirstThenZakura,
1715 _ => DualStackRoute::Passthrough,
1716 };
1717
1718 let legacy_enabled = self.legacy_enabled;
1719 let mut gossip = self.gossip.clone();
1720 let mut request_adapter = self.request.clone();
1721 let mut legacy = self.legacy.clone();
1722
1723 Box::pin(async move {
1724 match route {
1725 DualStackRoute::Advertise => {
1726 let gossip_fut = gossip.call(request.clone());
1730 let legacy_fut = async move {
1731 if legacy_enabled {
1732 Some(match legacy.ready().await {
1733 Ok(service) => service.call(request).await,
1734 Err(error) => Err(error),
1735 })
1736 } else {
1737 None
1738 }
1739 };
1740 let (legacy_res, gossip_res) = futures::join!(legacy_fut, gossip_fut);
1741 if let Some(Err(error)) = legacy_res {
1742 debug!(%error, "legacy advertise path failed; continuing");
1743 }
1744 if let Err(error) = gossip_res {
1745 debug!(%error, "zakura advertise path failed; continuing");
1746 }
1747 Ok(Response::Nil)
1748 }
1749 DualStackRoute::LegacyFirstThenZakura => {
1750 if matches!(request.inventory_source(), Some(PeerSource::Zakura(_))) {
1751 return request_adapter.call(request).await;
1752 }
1753 if !legacy_enabled {
1754 return request_adapter.call(request).await;
1755 }
1756 let legacy_attempt = timeout(DUAL_STACK_LEGACY_INVENTORY_TIMEOUT, async {
1761 legacy.ready().await?.call(request.clone()).await
1762 })
1763 .await;
1764 match legacy_attempt {
1765 Ok(Ok(response)) if !all_inventory_missing(&response) => Ok(response),
1766 Ok(Ok(response)) => match request_adapter.call(request).await {
1767 Ok(zakura) if !all_inventory_missing(&zakura) => Ok(zakura),
1768 _ => Ok(response),
1769 },
1770 Ok(Err(legacy_error)) => match request_adapter.call(request).await {
1771 Ok(zakura) => Ok(zakura),
1772 Err(_) => Err(legacy_error),
1773 },
1774 Err(_) => request_adapter.call(request).await,
1776 }
1777 }
1778 DualStackRoute::Passthrough => legacy.ready().await?.call(request).await,
1779 }
1780 })
1781 }
1782}
1783
1784#[derive(Clone, Debug)]
1786pub struct ZakuraRequestClient {
1787 supervisor: ZakuraSupervisorHandle,
1788 request_timeout: Duration,
1789 trace: ZakuraTrace,
1790}
1791
1792impl ZakuraRequestClient {
1793 pub fn new(supervisor: ZakuraSupervisorHandle) -> Self {
1795 Self::new_with_trace(supervisor, ZakuraTrace::noop())
1796 }
1797
1798 pub fn new_with_trace(supervisor: ZakuraSupervisorHandle, trace: ZakuraTrace) -> Self {
1800 Self {
1801 supervisor,
1802 request_timeout: LEGACY_REQUEST_TIMEOUT,
1803 trace,
1804 }
1805 }
1806
1807 #[cfg(test)]
1808 fn new_with_timeout(supervisor: ZakuraSupervisorHandle, request_timeout: Duration) -> Self {
1809 Self {
1810 supervisor,
1811 request_timeout,
1812 trace: ZakuraTrace::noop(),
1813 }
1814 }
1815
1816 async fn request(
1817 &self,
1818 frame: LegacyRequestFrame,
1819 source: Option<PeerSource>,
1820 retry_source_missing: bool,
1821 ) -> Result<Response, BoxError> {
1822 let preferred = match source {
1823 Some(PeerSource::Zakura(peer_id)) => Some(peer_id),
1824 _ => None,
1825 };
1826
1827 let handles = self.ready_handles().await?;
1828 let Some(primary) = select_handle(&handles, preferred.as_ref()) else {
1829 return Err("no ready Zakura peer for legacy inventory request".into());
1830 };
1831
1832 let request_kind = frame.kind();
1833 let first_result = self
1834 .request_one(primary.clone(), frame.clone(), request_kind)
1835 .await;
1836 match first_result {
1837 Ok(response) if !all_inventory_missing(&response) => Ok(response),
1838 Ok(response) => {
1839 let Some(fallback) = select_fallback_handle(&handles, primary.peer_id()) else {
1840 if retry_source_missing && preferred.is_some() {
1841 return self
1842 .retry_source_missing(primary, frame, request_kind, response)
1843 .await;
1844 }
1845 return Ok(response);
1846 };
1847 self.request_one(fallback, frame, request_kind)
1848 .await
1849 .or(Ok(response))
1850 }
1851 Err(error) => {
1852 let Some(fallback) = select_fallback_handle(&handles, primary.peer_id()) else {
1853 return Err(error);
1854 };
1855 self.request_one(fallback, frame, request_kind).await
1856 }
1857 }
1858 }
1859
1860 async fn retry_source_missing(
1861 &self,
1862 handle: ZakuraPeerHandle,
1863 frame: LegacyRequestFrame,
1864 request_kind: LegacyRequestKind,
1865 initial_response: Response,
1866 ) -> Result<Response, BoxError> {
1867 let mut last_response = initial_response;
1868 for _ in 0..SOURCE_INVENTORY_MISSING_RETRIES {
1869 sleep(SOURCE_INVENTORY_MISSING_RETRY_DELAY).await;
1870 match self
1871 .request_one(handle.clone(), frame.clone(), request_kind)
1872 .await
1873 {
1874 Ok(response) if !all_inventory_missing(&response) => return Ok(response),
1875 Ok(response) => last_response = response,
1876 Err(error) => return Err(error),
1877 }
1878 }
1879 Ok(last_response)
1880 }
1881
1882 async fn ready_handles(&self) -> Result<Vec<ZakuraPeerHandle>, BoxError> {
1883 let mut registered = self.supervisor.subscribe();
1884
1885 timeout(LEGACY_REQUEST_READY_TIMEOUT, async {
1886 loop {
1887 let handles = self.supervisor.outbound_peer_handles().await;
1888 if !handles.is_empty() {
1889 return Ok(handles);
1890 }
1891
1892 if registered.changed().await.is_err() {
1893 return Err("Zakura peer set closed before a peer was ready".into());
1894 }
1895 }
1896 })
1897 .await
1898 .map_err(|_| -> BoxError { "no ready Zakura peer for legacy inventory request".into() })?
1899 }
1900
1901 async fn request_one(
1902 &self,
1903 handle: ZakuraPeerHandle,
1904 frame: LegacyRequestFrame,
1905 request_kind: LegacyRequestKind,
1906 ) -> Result<Response, BoxError> {
1907 let request_id = NEXT_LEGACY_REQUEST_ID.fetch_add(1, Ordering::Relaxed);
1908 let requested_block_hashes: Option<HashSet<block::Hash>> = match &frame {
1911 LegacyRequestFrame::BlocksByHash(hashes) => Some(hashes.iter().copied().collect()),
1912 _ => None,
1913 };
1914 let frame = frame.encode_frame()?;
1915 trace_legacy_request_start(
1916 &self.trace,
1917 "outbound.request",
1918 Some(handle.peer_id()),
1919 request_id,
1920 request_kind,
1921 frame.message_type,
1922 );
1923 let started_at = Instant::now();
1924 let response = match timeout(
1925 self.request_timeout,
1926 handle.request(
1927 ZAKURA_STREAM_LEGACY_REQUESTS,
1928 request_id,
1929 frame.message_type,
1930 frame.flags,
1931 frame.payload,
1932 ),
1933 )
1934 .await
1935 {
1936 Ok(Ok(response)) => response,
1937 Ok(Err(error)) => {
1938 trace_legacy_request_error(
1939 &self.trace,
1940 "outbound.error",
1941 Some(handle.peer_id()),
1942 request_id,
1943 request_kind.command(),
1944 error.to_string(),
1945 );
1946 return Err(error);
1947 }
1948 Err(_) => {
1949 let error: BoxError = format!(
1950 "Zakura legacy request timed out for peer {:?}",
1951 handle.peer_id()
1952 )
1953 .into();
1954 trace_legacy_request_error(
1955 &self.trace,
1956 "outbound.error",
1957 Some(handle.peer_id()),
1958 request_id,
1959 request_kind.command(),
1960 error.to_string(),
1961 );
1962 return Err(error);
1963 }
1964 };
1965 let mut response = match LegacyResponseCodec::decode_response(
1966 request_id,
1967 request_kind,
1968 response,
1969 requested_block_hashes.as_ref(),
1970 ) {
1971 Ok(response) => response,
1972 Err(error) => {
1973 trace_legacy_request_error(
1974 &self.trace,
1975 "outbound.decode_error",
1976 Some(handle.peer_id()),
1977 request_id,
1978 request_kind.command(),
1979 error.to_string(),
1980 );
1981 return Err(error.into());
1982 }
1983 };
1984 if request_kind == LegacyRequestKind::Ping {
1985 response = Response::Pong(started_at.elapsed());
1987 }
1988 trace_legacy_request_response(
1989 &self.trace,
1990 "outbound.response",
1991 Some(handle.peer_id()),
1992 request_id,
1993 request_kind.command(),
1994 &response,
1995 );
1996 Ok(response)
1997 }
1998}
1999
2000fn select_handle(
2001 handles: &[ZakuraPeerHandle],
2002 preferred: Option<&ZakuraPeerId>,
2003) -> Option<ZakuraPeerHandle> {
2004 preferred
2008 .and_then(|peer_id| {
2009 handles
2010 .iter()
2011 .find(|handle| handle.peer_id() == peer_id)
2012 .cloned()
2013 })
2014 .or_else(|| handles.first().cloned())
2015}
2016
2017fn select_fallback_handle(
2018 handles: &[ZakuraPeerHandle],
2019 primary: &ZakuraPeerId,
2020) -> Option<ZakuraPeerHandle> {
2021 handles
2022 .iter()
2023 .find(|handle| handle.peer_id() != primary)
2024 .cloned()
2025}
2026
2027fn all_inventory_missing(response: &Response) -> bool {
2028 match response {
2029 Response::Blocks(blocks) => {
2030 blocks.is_empty() || blocks.iter().all(|block| block.is_missing())
2031 }
2032 Response::Transactions(transactions) => {
2033 transactions.is_empty()
2034 || transactions
2035 .iter()
2036 .all(|transaction| transaction.is_missing())
2037 }
2038 Response::BlockHashes(hashes) => hashes.is_empty(),
2039 Response::BlockHeaders(headers) => headers.is_empty(),
2040 Response::TransactionIds(ids) => ids.is_empty(),
2041 _ => false,
2042 }
2043}
2044
2045fn trace_legacy_request_start(
2046 trace: &ZakuraTrace,
2047 event: &'static str,
2048 peer: Option<&ZakuraPeerId>,
2049 request_id: u64,
2050 request_kind: LegacyRequestKind,
2051 message_type: u16,
2052) {
2053 trace.emit_with(LEGACY_REQUEST_TABLE, |row| {
2054 insert_trace_str(row, "event", event);
2055 insert_trace_peer(row, peer);
2056 insert_trace_u64(row, "request_id", request_id);
2057 insert_trace_str(row, "request", request_kind.command());
2058 insert_trace_u64(row, "message_type", u64::from(message_type));
2059 });
2060}
2061
2062fn trace_legacy_request_response(
2063 trace: &ZakuraTrace,
2064 event: &'static str,
2065 peer: Option<&ZakuraPeerId>,
2066 request_id: u64,
2067 request: &'static str,
2068 response: &Response,
2069) {
2070 let (response_kind, item_count, missing_count) = response_summary(response);
2071 trace.emit_with(LEGACY_REQUEST_TABLE, |row| {
2072 insert_trace_str(row, "event", event);
2073 insert_trace_peer(row, peer);
2074 insert_trace_u64(row, "request_id", request_id);
2075 insert_trace_str(row, "request", request);
2076 insert_trace_str(row, "response", response_kind);
2077 insert_trace_u64(row, "item_count", item_count);
2078 insert_trace_u64(row, "missing_count", missing_count);
2079 });
2080}
2081
2082fn trace_legacy_request_error(
2083 trace: &ZakuraTrace,
2084 event: &'static str,
2085 peer: Option<&ZakuraPeerId>,
2086 request_id: u64,
2087 request: &'static str,
2088 error: String,
2089) {
2090 trace.emit_with(LEGACY_REQUEST_TABLE, |row| {
2091 insert_trace_str(row, "event", event);
2092 insert_trace_peer(row, peer);
2093 insert_trace_u64(row, "request_id", request_id);
2094 insert_trace_str(row, "request", request);
2095 row.insert("error".to_string(), Value::String(error));
2096 });
2097}
2098
2099fn response_summary(response: &Response) -> (&'static str, u64, u64) {
2100 match response {
2101 Response::Blocks(blocks) => (
2102 "Blocks",
2103 bounded_u64(blocks.len()),
2104 bounded_u64(blocks.iter().filter(|block| block.is_missing()).count()),
2105 ),
2106 Response::Transactions(transactions) => (
2107 "Transactions",
2108 bounded_u64(transactions.len()),
2109 bounded_u64(
2110 transactions
2111 .iter()
2112 .filter(|transaction| transaction.is_missing())
2113 .count(),
2114 ),
2115 ),
2116 Response::BlockHashes(hashes) => ("BlockHashes", bounded_u64(hashes.len()), 0),
2117 Response::BlockHeaders(headers) => ("BlockHeaders", bounded_u64(headers.len()), 0),
2118 Response::TransactionIds(ids) => ("TransactionIds", bounded_u64(ids.len()), 0),
2119 Response::Pong(_) => ("Pong", 1, 0),
2120 Response::Nil => ("Nil", 0, 0),
2121 response => (response.command(), 0, 0),
2122 }
2123}
2124
2125fn insert_trace_peer(row: &mut Map<String, Value>, peer: Option<&ZakuraPeerId>) {
2126 row.insert(
2127 "peer".to_string(),
2128 peer.map_or(Value::Null, |peer| Value::String(trace_peer_label(peer))),
2129 );
2130}
2131
2132fn insert_trace_str(row: &mut Map<String, Value>, key: &'static str, value: &'static str) {
2133 row.insert(key.to_string(), Value::String(value.to_string()));
2134}
2135
2136fn insert_trace_u64(row: &mut Map<String, Value>, key: &'static str, value: u64) {
2137 row.insert(key.to_string(), Value::Number(Number::from(value)));
2138}
2139
2140fn bounded_u64(value: usize) -> u64 {
2141 u64::try_from(value).unwrap_or(u64::MAX)
2142}
2143
2144#[derive(Clone, Debug)]
2145struct LegacyGossipForwarder {
2146 broadcast: ZakuraGossipBroadcast,
2147 attempt_cooldown: FirstSeenCache,
2151}
2152
2153impl LegacyGossipForwarder {
2154 fn new(supervisor: ZakuraSupervisorHandle) -> Self {
2155 Self {
2156 broadcast: ZakuraGossipBroadcast::new(supervisor),
2157 attempt_cooldown: FirstSeenCache::new(
2158 DEFAULT_FIRST_SEEN_CAPACITY,
2159 LEGACY_GOSSIP_DUPLICATE_COOLDOWN,
2160 ),
2161 }
2162 }
2163
2164 async fn unseen(&self, frame: &LegacyGossipFrame) -> Option<LegacyGossipFrame> {
2165 self.broadcast.unseen(frame).await
2166 }
2167
2168 async fn fresh_attempt(&self, frame: &LegacyGossipFrame) -> Option<LegacyGossipFrame> {
2172 self.attempt_cooldown.unseen(frame).await
2173 }
2174
2175 async fn note_failed_attempt(&self, frame: &LegacyGossipFrame, elapsed: Duration) {
2179 if elapsed >= LEGACY_GOSSIP_EXPENSIVE_ATTEMPT {
2180 self.attempt_cooldown.record_seen(frame).await;
2181 }
2182 }
2183
2184 async fn mark_seen(&self, frame: &LegacyGossipFrame) {
2185 self.broadcast.mark_seen(frame).await;
2186 }
2187
2188 async fn forward(
2189 &self,
2190 peer_id: &ZakuraPeerId,
2191 frame: LegacyGossipFrame,
2192 ) -> Result<(), BoxError> {
2193 self.broadcast.broadcast(frame, Some(peer_id)).await
2194 }
2195}
2196
2197#[derive(Debug)]
2199pub struct LegacyGossipSink {
2200 inbound_tx: mpsc::Sender<LegacyInboundWork>,
2201 outbound: LegacyGossipOutbound,
2202 trace: ZakuraTrace,
2203}
2204
2205impl LegacyGossipSink {
2206 pub fn spawn<Inbound>(inbound: Inbound, supervisor: ZakuraSupervisorHandle) -> Self
2208 where
2209 Inbound: Service<Request, Response = Response, Error = BoxError> + Send + Clone + 'static,
2210 Inbound::Future: Send + 'static,
2211 {
2212 Self::spawn_with_trace(inbound, supervisor, ZakuraTrace::noop())
2213 }
2214
2215 pub fn spawn_with_trace<Inbound>(
2217 inbound: Inbound,
2218 supervisor: ZakuraSupervisorHandle,
2219 trace: ZakuraTrace,
2220 ) -> Self
2221 where
2222 Inbound: Service<Request, Response = Response, Error = BoxError> + Send + Clone + 'static,
2223 Inbound::Future: Send + 'static,
2224 {
2225 let (inbound_tx, inbound_rx) = mpsc::channel(LEGACY_GOSSIP_INBOUND_QUEUE);
2226 tokio::spawn(legacy_gossip_worker(
2227 inbound,
2228 inbound_rx,
2229 LegacyGossipForwarder::new(supervisor.clone()),
2230 trace.clone(),
2231 ));
2232 let outbound = outbound_for_supervisor(&supervisor);
2233 Self {
2234 inbound_tx,
2235 outbound,
2236 trace,
2237 }
2238 }
2239}
2240
2241impl LegacyGossipSink {
2242 fn enqueue_gossip_frame(
2243 inbound_tx: &mpsc::Sender<LegacyInboundWork>,
2244 peer_id: ZakuraPeerId,
2245 frame: Frame,
2246 ) -> Result<(), SinkReject> {
2247 let frame = LegacyGossipFrame::decode_frame(frame).map_err(SinkReject::protocol)?;
2248 match inbound_tx.try_send(LegacyInboundWork::Gossip(LegacyGossipInbound {
2249 peer_id,
2250 frame,
2251 })) {
2252 Ok(()) => Ok(()),
2253 Err(mpsc::error::TrySendError::Full(_)) => {
2254 debug!("legacy gossip inbound queue full: dropping frame");
2255 Ok(())
2256 }
2257 Err(mpsc::error::TrySendError::Closed(_)) => {
2258 Err(SinkReject::local("legacy gossip inbound queue closed"))
2259 }
2260 }
2261 }
2262
2263 fn deliver(
2264 &self,
2265 peer_id: ZakuraPeerId,
2266 stream_kind: u16,
2267 frame: Frame,
2268 ) -> Result<(), SinkReject> {
2269 if stream_kind != ZAKURA_STREAM_GOSSIP {
2270 return Ok(());
2271 }
2272
2273 Self::enqueue_gossip_frame(&self.inbound_tx, peer_id, frame)
2274 }
2275
2276 fn request<'a>(
2277 &'a self,
2278 peer_id: ZakuraPeerId,
2279 stream_kind: u16,
2280 request_id: u64,
2281 max_frame_bytes: u32,
2282 max_message_bytes: u32,
2283 frame: Frame,
2284 ) -> BoxRunFuture<'a, Result<Vec<Frame>, SinkReject>> {
2285 Box::pin(async move {
2286 if stream_kind != ZAKURA_STREAM_LEGACY_REQUESTS {
2287 return Err(SinkReject::protocol(
2288 "unsupported legacy request stream kind",
2289 ));
2290 }
2291
2292 let frame = LegacyRequestFrame::decode_frame(frame).map_err(SinkReject::protocol)?;
2293 let (response_tx, response_rx) = oneshot::channel();
2294 match self
2295 .inbound_tx
2296 .try_send(LegacyInboundWork::Request(LegacyRequestInbound {
2297 peer_id: peer_id.clone(),
2298 request_id,
2299 frame,
2300 response_tx,
2301 })) {
2302 Ok(()) => {}
2303 Err(mpsc::error::TrySendError::Full(_)) => {
2304 return Err(SinkReject::local("legacy request inbound queue full"));
2305 }
2306 Err(mpsc::error::TrySendError::Closed(_)) => {
2307 return Err(SinkReject::local("legacy request inbound queue closed"));
2308 }
2309 }
2310
2311 let response = timeout(LEGACY_REQUEST_TIMEOUT, response_rx)
2312 .await
2313 .map_err(|_| SinkReject::local("legacy request service timed out"))?
2314 .map_err(|_| SinkReject::local("legacy request response dropped"))?
2315 .map_err(SinkReject::local)?;
2316 trace_legacy_request_response(
2317 &self.trace,
2318 "inbound.response",
2319 Some(&peer_id),
2320 request_id,
2321 response.command(),
2322 &response,
2323 );
2324 LegacyResponseCodec::encode_response(
2325 request_id,
2326 response,
2327 max_frame_bytes,
2328 max_message_bytes,
2329 )
2330 .map_err(SinkReject::local)
2331 })
2332 }
2333}
2334
2335impl ZakuraService for LegacyGossipSink {
2336 fn name(&self) -> &'static str {
2337 "legacy-gossip"
2338 }
2339
2340 fn streams(&self) -> &[Stream] {
2341 legacy_gossip_streams()
2342 }
2343
2344 fn add_peer(&self, mut peer: Peer) {
2345 let Some((mut recv, send)) = peer.take_stream(ZAKURA_STREAM_GOSSIP) else {
2346 return;
2347 };
2348 let outbound = self.outbound.clone();
2349 let inbound_tx = self.inbound_tx.clone();
2350 let peer_id = peer.id.clone();
2351 let conn_id = peer.conn_id;
2352 let cancel_token = peer.cancel_token();
2353 let session = LegacyGossipPeerSession::new(peer_id.clone(), conn_id, send);
2354
2355 if !outbound.insert(session.clone()) {
2356 return;
2357 }
2358 let replay_task_peer_id = peer_id.clone();
2359 let replay_panic_peer_id = replay_task_peer_id.clone();
2360 let replay_panic_outbound = outbound.clone();
2361 let replay_panic_cancel = cancel_token.clone();
2362 spawn_supervised_peer_task(
2363 replay_task_peer_id,
2364 || {},
2365 move || {
2366 replay_panic_cancel.cancel();
2367 replay_panic_outbound.remove(&replay_panic_peer_id, conn_id);
2368 },
2369 {
2370 let outbound = outbound.clone();
2371 let session = session.clone();
2372 async move {
2373 if let Err(error) = outbound.replay_latest_block_to_peer(session).await {
2374 debug!(?error, "latest Zakura block gossip replay failed");
2375 }
2376 }
2377 },
2378 );
2379
2380 let recv_task_peer_id = peer_id.clone();
2381 let recv_panic_peer_id = recv_task_peer_id.clone();
2382 let recv_panic_outbound = outbound.clone();
2383 let recv_panic_cancel = cancel_token.clone();
2384 spawn_supervised_peer_task(
2385 recv_task_peer_id,
2386 || {},
2387 move || {
2388 recv_panic_cancel.cancel();
2389 recv_panic_outbound.remove(&recv_panic_peer_id, conn_id);
2390 },
2391 async move {
2392 loop {
2393 let frame = tokio::select! {
2394 _ = cancel_token.cancelled() => {
2395 outbound.remove(&peer_id, conn_id);
2396 return;
2397 }
2398 frame = recv.recv() => {
2399 let Some(frame) = frame else {
2400 outbound.remove(&peer_id, conn_id);
2401 return;
2402 };
2403 frame
2404 }
2405 };
2406
2407 #[cfg(test)]
2408 if should_panic_legacy_gossip_recv_loop(&peer_id) {
2409 panic!("injected legacy gossip recv-loop panic after state registration");
2410 }
2411
2412 match Self::enqueue_gossip_frame(&inbound_tx, peer_id.clone(), frame) {
2413 Ok(()) => {}
2414 Err(SinkReject::Protocol(error)) => {
2415 debug!(
2416 ?error,
2417 ?peer_id,
2418 "legacy gossip stream rejected protocol-invalid frame"
2419 );
2420 cancel_token.cancel();
2421 outbound.remove(&peer_id, conn_id);
2422 return;
2423 }
2424 Err(SinkReject::Local(error)) => {
2425 debug!(?error, ?peer_id, "legacy gossip inbound queue closed");
2426 return;
2427 }
2428 }
2429 }
2430 },
2431 );
2432 }
2433
2434 fn remove_peer(&self, peer: &ZakuraPeerId, conn_id: ZakuraConnId) {
2435 self.outbound.remove(peer, conn_id);
2436 }
2437
2438 fn deliver_frame(
2439 &self,
2440 peer_id: ZakuraPeerId,
2441 stream_kind: u16,
2442 frame: Frame,
2443 ) -> Result<(), SinkReject> {
2444 self.deliver(peer_id, stream_kind, frame)
2445 }
2446
2447 fn as_request_response(&self) -> Option<&dyn RequestResponseService> {
2448 Some(self)
2449 }
2450}
2451
2452impl RequestResponseService for LegacyGossipSink {
2453 fn request_frame<'a>(
2454 &'a self,
2455 peer_id: ZakuraPeerId,
2456 stream_kind: u16,
2457 request_id: u64,
2458 max_frame_bytes: u32,
2459 max_message_bytes: u32,
2460 frame: Frame,
2461 ) -> BoxRunFuture<'a, Result<Vec<Frame>, SinkReject>> {
2462 self.request(
2463 peer_id,
2464 stream_kind,
2465 request_id,
2466 max_frame_bytes,
2467 max_message_bytes,
2468 frame,
2469 )
2470 }
2471}
2472
2473#[derive(Debug)]
2474enum LegacyInboundWork {
2475 Gossip(LegacyGossipInbound),
2476 Request(LegacyRequestInbound),
2477}
2478
2479#[derive(Debug)]
2480struct LegacyGossipInbound {
2481 peer_id: ZakuraPeerId,
2482 frame: LegacyGossipFrame,
2483}
2484
2485#[derive(Debug)]
2486struct LegacyRequestInbound {
2487 peer_id: ZakuraPeerId,
2488 request_id: u64,
2489 frame: LegacyRequestFrame,
2490 response_tx: oneshot::Sender<Result<Response, BoxError>>,
2491}
2492
2493async fn legacy_gossip_worker<Inbound>(
2494 mut inbound: Inbound,
2495 mut inbound_rx: mpsc::Receiver<LegacyInboundWork>,
2496 forwarder: LegacyGossipForwarder,
2497 trace: ZakuraTrace,
2498) where
2499 Inbound: Service<Request, Response = Response, Error = BoxError> + Send + Clone + 'static,
2500 Inbound::Future: Send + 'static,
2501{
2502 let request_permits = Arc::new(Semaphore::new(LEGACY_REQUEST_IN_FLIGHT_LIMIT));
2503
2504 while let Some(work) = inbound_rx.recv().await {
2505 match work {
2506 LegacyInboundWork::Gossip(gossip) => {
2507 handle_legacy_gossip(&mut inbound, gossip, &forwarder).await;
2508 }
2509 LegacyInboundWork::Request(request) => {
2510 let Ok(permit) = request_permits.clone().try_acquire_owned() else {
2511 let _ = request.response_tx.send(Err(
2512 "legacy request inbound concurrency limit reached".into(),
2513 ));
2514 continue;
2515 };
2516 tokio::spawn(handle_legacy_request(
2517 inbound.clone(),
2518 request,
2519 permit,
2520 trace.clone(),
2521 ));
2522 }
2523 }
2524 }
2525}
2526
2527async fn handle_legacy_gossip<Inbound>(
2528 inbound: &mut Inbound,
2529 gossip: LegacyGossipInbound,
2530 forwarder: &LegacyGossipForwarder,
2531) where
2532 Inbound: Service<Request, Response = Response, Error = BoxError> + Send + 'static,
2533 Inbound::Future: Send + 'static,
2534{
2535 let Some(unseen_frame) = forwarder.unseen(&gossip.frame).await else {
2536 return;
2537 };
2538 let Some(unseen_frame) = forwarder.fresh_attempt(&unseen_frame).await else {
2545 debug!("legacy gossip duplicate suppressed after recent expensive attempt");
2546 return;
2547 };
2548 let request = unseen_frame.clone().into_request(gossip.peer_id.clone());
2549
2550 let started = Instant::now();
2551 let ready = timeout(LEGACY_GOSSIP_SERVICE_TIMEOUT, inbound.ready()).await;
2552 let Ok(Ok(service)) = ready else {
2553 debug!("legacy gossip inbound service was not ready");
2554 forwarder
2555 .note_failed_attempt(&unseen_frame, started.elapsed())
2556 .await;
2557 return;
2558 };
2559
2560 match timeout(LEGACY_GOSSIP_SERVICE_TIMEOUT, service.call(request)).await {
2561 Ok(Ok(_)) => {}
2562 Ok(Err(error)) => {
2563 debug!(?error, "legacy gossip inbound service call failed");
2564 forwarder
2565 .note_failed_attempt(&unseen_frame, started.elapsed())
2566 .await;
2567 return;
2568 }
2569 Err(_) => {
2570 debug!("legacy gossip inbound service call timed out");
2571 forwarder
2572 .note_failed_attempt(&unseen_frame, started.elapsed())
2573 .await;
2574 return;
2575 }
2576 }
2577
2578 forwarder.mark_seen(&unseen_frame).await;
2579 if let Err(error) = forwarder.forward(&gossip.peer_id, unseen_frame).await {
2580 debug!(?error, "legacy gossip forward failed");
2581 }
2582}
2583
2584async fn handle_legacy_request<Inbound>(
2585 mut inbound: Inbound,
2586 request: LegacyRequestInbound,
2587 _permit: OwnedSemaphorePermit,
2588 trace: ZakuraTrace,
2589) where
2590 Inbound: Service<Request, Response = Response, Error = BoxError> + Send + 'static,
2591 Inbound::Future: Send + 'static,
2592{
2593 let command = request.frame.to_string();
2594 let peer_id = request.peer_id.clone();
2595 let request_id = request.request_id;
2596 let request_kind = request.frame.kind();
2597 let mut response_tx = request.response_tx;
2598 let Some(legacy_request) = request.frame.into_service_request(peer_id.clone()) else {
2599 let _ = response_tx.send(Ok(Response::Pong(Duration::ZERO)));
2601 return;
2602 };
2603 trace_legacy_request_start(
2604 &trace,
2605 "inbound.request",
2606 Some(&peer_id),
2607 request_id,
2608 request_kind,
2609 request_kind.message_type(),
2610 );
2611
2612 let service_work = async move {
2613 let ready = timeout(LEGACY_REQUEST_TIMEOUT, inbound.ready()).await;
2614 match ready {
2615 Ok(Ok(service)) => timeout(LEGACY_REQUEST_TIMEOUT, service.call(legacy_request))
2616 .await
2617 .map_err(|_| -> BoxError { format!("{command} inbound service timed out").into() })
2618 .and_then(|response| response),
2619 Ok(Err(error)) => Err(error),
2620 Err(_) => Err(format!("{command} inbound service readiness timed out").into()),
2621 }
2622 };
2623
2624 let result = tokio::select! {
2632 biased;
2633 () = response_tx.closed() => {
2634 debug!(
2635 ?peer_id,
2636 request_id,
2637 "legacy request handler aborted: response receiver dropped before service completed"
2638 );
2639 return;
2640 }
2641 result = service_work => result,
2642 };
2643
2644 if let Err(error) = &result {
2645 trace_legacy_request_error(
2646 &trace,
2647 "inbound.error",
2648 Some(&peer_id),
2649 request_id,
2650 request_kind.command(),
2651 error.to_string(),
2652 );
2653 }
2654
2655 if response_tx.send(result).is_err() {
2656 debug!(
2657 ?peer_id,
2658 request_id, "legacy request response receiver dropped before service completed"
2659 );
2660 }
2661}
2662
2663#[derive(Clone, Debug)]
2664struct FirstSeenCache {
2665 inner: std::sync::Arc<Mutex<FirstSeenCacheInner>>,
2666}
2667
2668impl FirstSeenCache {
2669 fn new(capacity: usize, ttl: Duration) -> Self {
2670 Self {
2671 inner: std::sync::Arc::new(Mutex::new(FirstSeenCacheInner {
2672 capacity: capacity.max(1),
2673 ttl,
2674 entries: HashMap::new(),
2675 order: VecDeque::new(),
2676 })),
2677 }
2678 }
2679
2680 async fn record_unseen(
2681 &self,
2682 keys: impl IntoIterator<Item = InventoryKey>,
2683 ) -> Vec<InventoryKey> {
2684 let mut inner = self.inner.lock().await;
2685 let now = Instant::now();
2686 inner.prune(now);
2687 let mut recorded = Vec::new();
2688 for key in keys {
2689 if inner.insert(key, now) {
2690 recorded.push(key);
2691 }
2692 }
2693 recorded
2694 }
2695
2696 async fn record_first_seen(&self, frame: &LegacyGossipFrame) -> Option<LegacyGossipFrame> {
2697 let unseen = self.unseen(frame).await?;
2698 self.record_seen(&unseen).await;
2699 Some(unseen)
2700 }
2701
2702 async fn unseen(&self, frame: &LegacyGossipFrame) -> Option<LegacyGossipFrame> {
2703 match frame {
2704 LegacyGossipFrame::AdvertiseBlock(hash) => {
2705 (!self.contains(InventoryKey::Block(*hash)).await)
2706 .then_some(LegacyGossipFrame::AdvertiseBlock(*hash))
2707 }
2708 LegacyGossipFrame::AdvertiseTransactionIds(ids) => {
2709 let unseen = self.unrecorded_transaction_ids(ids.iter().copied()).await;
2710 (!unseen.is_empty()).then_some(LegacyGossipFrame::AdvertiseTransactionIds(unseen))
2711 }
2712 }
2713 }
2714
2715 async fn record_seen(&self, frame: &LegacyGossipFrame) {
2716 match frame {
2717 LegacyGossipFrame::AdvertiseBlock(hash) => {
2718 self.record_unseen([InventoryKey::Block(*hash)]).await;
2719 }
2720 LegacyGossipFrame::AdvertiseTransactionIds(ids) => {
2721 self.record_unseen(ids.iter().copied().map(InventoryKey::Transaction))
2722 .await;
2723 }
2724 }
2725 }
2726
2727 async fn contains(&self, key: InventoryKey) -> bool {
2728 let mut inner = self.inner.lock().await;
2729 let now = Instant::now();
2730 inner.prune(now);
2731 inner.entries.contains_key(&key)
2732 }
2733
2734 async fn unrecorded_transaction_ids(
2735 &self,
2736 ids: impl IntoIterator<Item = UnminedTxId>,
2737 ) -> Vec<UnminedTxId> {
2738 let mut inner = self.inner.lock().await;
2739 inner.prune(Instant::now());
2740 let mut unseen = Vec::new();
2741 for tx_id in ids {
2742 if !inner
2743 .entries
2744 .contains_key(&InventoryKey::Transaction(tx_id))
2745 {
2746 unseen.push(tx_id);
2747 }
2748 }
2749 unseen
2750 }
2751}
2752
2753#[derive(Debug)]
2754struct FirstSeenCacheInner {
2755 capacity: usize,
2756 ttl: Duration,
2757 entries: HashMap<InventoryKey, Instant>,
2758 order: VecDeque<InventoryKey>,
2759}
2760
2761impl FirstSeenCacheInner {
2762 fn insert(&mut self, key: InventoryKey, now: Instant) -> bool {
2763 if self.entries.contains_key(&key) {
2764 return false;
2765 }
2766 self.entries.insert(key, now);
2767 self.order.push_back(key);
2768 while self.entries.len() > self.capacity {
2769 if let Some(oldest) = self.order.pop_front() {
2770 self.entries.remove(&oldest);
2771 }
2772 }
2773 true
2774 }
2775
2776 fn prune(&mut self, now: Instant) {
2777 while let Some(key) = self.order.front().copied() {
2778 let Some(seen_at) = self.entries.get(&key).copied() else {
2779 self.order.pop_front();
2780 continue;
2781 };
2782 if now.saturating_duration_since(seen_at) < self.ttl {
2783 break;
2784 }
2785 self.order.pop_front();
2786 self.entries.remove(&key);
2787 }
2788 }
2789}
2790
2791#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
2792enum InventoryKey {
2793 Block(block::Hash),
2794 Transaction(UnminedTxId),
2795}
2796
2797#[derive(Debug, Error)]
2799pub enum LegacyGossipError {
2800 #[error("unsupported legacy gossip request: {0}")]
2802 UnsupportedRequest(&'static str),
2803 #[error("unsupported legacy gossip flags: {0}")]
2805 UnsupportedFlags(u16),
2806 #[error("unknown legacy gossip message type: {0}")]
2808 UnknownMessageType(u16),
2809 #[error("empty transaction-id advertisement")]
2811 EmptyTransactionAdvertisement,
2812 #[error("too many inventory items in legacy request/response: {0}")]
2814 TooManyInventoryItems(usize),
2815 #[error("too many block locator hashes in legacy request: {0}")]
2817 TooManyBlockLocatorHashes(usize),
2818 #[error("too many block headers in legacy response: {0}")]
2820 TooManyHeaders(usize),
2821 #[error("transaction-id advertisement contained non-transaction inventory")]
2823 NonTransactionInventory,
2824 #[error("trailing bytes after legacy gossip payload")]
2826 TrailingBytes,
2827 #[error("wrong legacy request id in response: expected {expected}, got {actual}")]
2829 WrongRequestId {
2830 expected: u64,
2832 actual: u64,
2834 },
2835 #[error("incomplete legacy response chunk")]
2837 IncompleteResponseChunk,
2838 #[error("truncated legacy response")]
2840 TruncatedResponse,
2841 #[error("oversized legacy response: {0} bytes")]
2843 OversizedResponse(usize),
2844 #[error("legacy response exceeded responder aggregate budget: {0} bytes")]
2846 ResponseAggregateBudget(usize),
2847 #[error("unexpected legacy response: {0}")]
2849 UnexpectedResponse(&'static str),
2850 #[error("missing legacy response: {0}")]
2852 MissingResponse(&'static str),
2853 #[error("legacy block response contained an unrequested block: {0:?}")]
2856 UnsolicitedBlock(block::Hash),
2857 #[error(transparent)]
2859 Serialization(#[from] SerializationError),
2860 #[error(transparent)]
2862 Io(#[from] std::io::Error),
2863 #[error(transparent)]
2865 Integer(#[from] std::num::TryFromIntError),
2866}
2867
2868impl fmt::Display for LegacyGossipFrame {
2869 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2870 match self {
2871 Self::AdvertiseBlock(_) => f.write_str("AdvertiseBlock"),
2872 Self::AdvertiseTransactionIds(ids) => {
2873 write!(f, "AdvertiseTransactionIds({})", ids.len())
2874 }
2875 }
2876 }
2877}
2878
2879impl fmt::Display for LegacyRequestFrame {
2880 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2881 match self {
2882 Self::BlocksByHash(hashes) => write!(f, "BlocksByHash({})", hashes.len()),
2883 Self::TransactionsById(ids) => write!(f, "TransactionsById({})", ids.len()),
2884 Self::FindBlocks { known_blocks, stop } => write!(
2885 f,
2886 "FindBlocks {{ known_blocks: {}, stop: {} }}",
2887 known_blocks.len(),
2888 stop.is_some()
2889 ),
2890 Self::FindHeaders { known_blocks, stop } => write!(
2891 f,
2892 "FindHeaders {{ known_blocks: {}, stop: {} }}",
2893 known_blocks.len(),
2894 stop.is_some()
2895 ),
2896 Self::MempoolTransactionIds => f.write_str("MempoolTransactionIds"),
2897 Self::Ping => f.write_str("Ping"),
2898 Self::PushTransaction(_) => f.write_str("PushTransaction"),
2899 }
2900 }
2901}
2902
2903#[cfg(test)]
2904mod tests {
2905 use super::*;
2906 use std::{
2907 collections::HashSet,
2908 sync::{
2909 atomic::{AtomicUsize, Ordering},
2910 Arc,
2911 },
2912 task::{Context, Poll},
2913 };
2914
2915 use futures::FutureExt;
2916 use tokio::sync::mpsc::UnboundedReceiver;
2917 use tower::ServiceExt;
2918 use zakura_chain::{
2919 parameters::NetworkUpgrade,
2920 serialization::{ZcashDeserialize, ZcashSerialize},
2921 transaction::{self, LockTime, Transaction, WtxId},
2922 };
2923 use zakura_test::vectors::BLOCK_TESTNET_141042_BYTES;
2924
2925 use crate::zakura::{
2926 framed_channel,
2927 testkit::{HostilePeer, ZakuraTestNode, TEST_NET_TIMEOUT},
2928 Peer, ServicePeerDirection, ZAKURA_CAP_LEGACY_GOSSIP,
2929 };
2930
2931 fn block_hash(byte: u8) -> block::Hash {
2932 block::Hash([byte; 32])
2933 }
2934
2935 fn legacy_tx_id(byte: u8) -> UnminedTxId {
2936 UnminedTxId::from_legacy_id(transaction::Hash([byte; 32]))
2937 }
2938
2939 fn witnessed_tx_id(byte: u8) -> UnminedTxId {
2940 UnminedTxId::from(WtxId {
2941 id: transaction::Hash([byte; 32]),
2942 auth_digest: transaction::AuthDigest::from([byte.wrapping_add(1); 32]),
2943 })
2944 }
2945
2946 fn empty_v5_transaction(byte: u8) -> Transaction {
2947 Transaction::V5 {
2948 network_upgrade: NetworkUpgrade::Nu5,
2949 lock_time: LockTime::min_lock_time_timestamp(),
2950 expiry_height: block::Height(u32::from(byte)),
2951 inputs: Vec::new(),
2952 outputs: Vec::new(),
2953 sapling_shielded_data: None,
2954 orchard_shielded_data: None,
2955 }
2956 }
2957
2958 fn encoded_count(count: usize) -> Vec<u8> {
2959 CompactSizeMessage::try_from(count)
2960 .expect("test count is within CompactSizeMessage bounds")
2961 .zcash_serialize_to_vec()
2962 .expect("compact size serializes")
2963 }
2964
2965 #[derive(Clone, Debug)]
2966 struct RequestRecorder {
2967 tx: tokio::sync::mpsc::UnboundedSender<Request>,
2968 }
2969
2970 impl Service<Request> for RequestRecorder {
2971 type Response = Response;
2972 type Error = BoxError;
2973 type Future = std::future::Ready<Result<Response, BoxError>>;
2974
2975 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2976 Poll::Ready(Ok(()))
2977 }
2978
2979 fn call(&mut self, request: Request) -> Self::Future {
2980 match self.tx.send(request) {
2981 Ok(()) => std::future::ready(Ok(Response::Nil)),
2982 Err(_) => std::future::ready(Err("request recorder receiver dropped".into())),
2983 }
2984 }
2985 }
2986
2987 #[derive(Clone, Debug)]
2988 struct FailsOnceRecorder {
2989 attempts: Arc<AtomicUsize>,
2990 tx: tokio::sync::mpsc::UnboundedSender<Request>,
2991 }
2992
2993 impl Service<Request> for FailsOnceRecorder {
2994 type Response = Response;
2995 type Error = BoxError;
2996 type Future = std::future::Ready<Result<Response, BoxError>>;
2997
2998 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2999 Poll::Ready(Ok(()))
3000 }
3001
3002 fn call(&mut self, request: Request) -> Self::Future {
3003 if self.attempts.fetch_add(1, Ordering::SeqCst) == 0 {
3004 return std::future::ready(Err("intentional transient inbound failure".into()));
3005 }
3006
3007 match self.tx.send(request) {
3008 Ok(()) => std::future::ready(Ok(Response::Nil)),
3009 Err(_) => std::future::ready(Err("request recorder receiver dropped".into())),
3010 }
3011 }
3012 }
3013
3014 #[derive(Clone, Debug)]
3015 struct InventoryResponder {
3016 transaction: UnminedTx,
3017 }
3018
3019 impl Service<Request> for InventoryResponder {
3020 type Response = Response;
3021 type Error = BoxError;
3022 type Future = std::future::Ready<Result<Response, BoxError>>;
3023
3024 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
3025 Poll::Ready(Ok(()))
3026 }
3027
3028 fn call(&mut self, request: Request) -> Self::Future {
3029 let response = match request {
3030 Request::BlocksByHash(hashes) | Request::BlocksByHashFrom { hashes, .. } => {
3031 Response::Blocks(hashes.into_iter().map(InventoryResponse::Missing).collect())
3032 }
3033 Request::TransactionsById(ids) | Request::TransactionsByIdFrom { ids, .. } => {
3034 Response::Transactions(
3035 ids.into_iter()
3036 .map(|id| {
3037 if id == self.transaction.id {
3038 InventoryResponse::Available((self.transaction.clone(), None))
3039 } else {
3040 InventoryResponse::Missing(id)
3041 }
3042 })
3043 .collect(),
3044 )
3045 }
3046 request => {
3047 return std::future::ready(Err(format!(
3048 "unexpected inventory request: {request:?}"
3049 )
3050 .into()));
3051 }
3052 };
3053 std::future::ready(Ok(response))
3054 }
3055 }
3056
3057 #[derive(Clone, Debug)]
3058 struct BlockInventoryResponder {
3059 block: Arc<Block>,
3060 }
3061
3062 impl Service<Request> for BlockInventoryResponder {
3063 type Response = Response;
3064 type Error = BoxError;
3065 type Future = std::future::Ready<Result<Response, BoxError>>;
3066
3067 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
3068 Poll::Ready(Ok(()))
3069 }
3070
3071 fn call(&mut self, request: Request) -> Self::Future {
3072 let response = match request {
3073 Request::BlocksByHash(hashes) | Request::BlocksByHashFrom { hashes, .. } => {
3074 Response::Blocks(
3075 hashes
3076 .into_iter()
3077 .map(|hash| {
3078 if hash == self.block.hash() {
3079 InventoryResponse::Available((self.block.clone(), None))
3080 } else {
3081 InventoryResponse::Missing(hash)
3082 }
3083 })
3084 .collect(),
3085 )
3086 }
3087 request => {
3088 return std::future::ready(Err(format!(
3089 "unexpected block inventory request: {request:?}"
3090 )
3091 .into()));
3092 }
3093 };
3094 std::future::ready(Ok(response))
3095 }
3096 }
3097
3098 #[derive(Clone, Debug)]
3099 struct SlowRequestThenRecorder {
3100 release: tokio::sync::watch::Receiver<bool>,
3101 tx: tokio::sync::mpsc::UnboundedSender<Request>,
3102 }
3103
3104 impl Service<Request> for SlowRequestThenRecorder {
3105 type Response = Response;
3106 type Error = BoxError;
3107 type Future = Pin<Box<dyn Future<Output = Result<Response, BoxError>> + Send>>;
3108
3109 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
3110 Poll::Ready(Ok(()))
3111 }
3112
3113 fn call(&mut self, request: Request) -> Self::Future {
3114 match request {
3115 Request::BlocksByHash(hashes) | Request::BlocksByHashFrom { hashes, .. } => {
3116 let mut release = self.release.clone();
3117 async move {
3118 while !*release.borrow() {
3119 if release.changed().await.is_err() {
3120 break;
3121 }
3122 }
3123 Ok(Response::Blocks(
3124 hashes.into_iter().map(InventoryResponse::Missing).collect(),
3125 ))
3126 }
3127 .boxed()
3128 }
3129 request => {
3130 let tx = self.tx.clone();
3131 async move {
3132 tx.send(request)?;
3133 Ok(Response::Nil)
3134 }
3135 .boxed()
3136 }
3137 }
3138 }
3139 }
3140
3141 #[derive(Clone, Debug)]
3144 struct NeverCompletesService;
3145
3146 impl Service<Request> for NeverCompletesService {
3147 type Response = Response;
3148 type Error = BoxError;
3149 type Future = Pin<Box<dyn Future<Output = Result<Response, BoxError>> + Send>>;
3150
3151 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
3152 Poll::Ready(Ok(()))
3153 }
3154
3155 fn call(&mut self, _request: Request) -> Self::Future {
3156 std::future::pending().boxed()
3157 }
3158 }
3159
3160 #[derive(Clone, Debug)]
3166 struct CountingPendingService {
3167 calls: Arc<AtomicUsize>,
3168 }
3169
3170 impl Service<Request> for CountingPendingService {
3171 type Response = Response;
3172 type Error = BoxError;
3173 type Future = Pin<Box<dyn Future<Output = Result<Response, BoxError>> + Send>>;
3174
3175 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
3176 Poll::Ready(Ok(()))
3177 }
3178
3179 fn call(&mut self, _request: Request) -> Self::Future {
3180 self.calls.fetch_add(1, Ordering::SeqCst);
3181 std::future::pending().boxed()
3182 }
3183 }
3184
3185 #[derive(Clone, Debug)]
3186 struct RecordingInventoryResponder {
3187 transaction: Option<UnminedTx>,
3188 tx: tokio::sync::mpsc::UnboundedSender<Request>,
3189 }
3190
3191 impl Service<Request> for RecordingInventoryResponder {
3192 type Response = Response;
3193 type Error = BoxError;
3194 type Future = std::future::Ready<Result<Response, BoxError>>;
3195
3196 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
3197 Poll::Ready(Ok(()))
3198 }
3199
3200 fn call(&mut self, request: Request) -> Self::Future {
3201 if let Err(error) = self.tx.send(request.clone()) {
3202 return std::future::ready(Err(Box::new(error)));
3203 }
3204
3205 let response = match request {
3206 Request::BlocksByHash(hashes) | Request::BlocksByHashFrom { hashes, .. } => {
3207 Response::Blocks(hashes.into_iter().map(InventoryResponse::Missing).collect())
3208 }
3209 Request::TransactionsById(ids) | Request::TransactionsByIdFrom { ids, .. } => {
3210 Response::Transactions(
3211 ids.into_iter()
3212 .map(|id| match &self.transaction {
3213 Some(transaction) if id == transaction.id => {
3214 InventoryResponse::Available((transaction.clone(), None))
3215 }
3216 _ => InventoryResponse::Missing(id),
3217 })
3218 .collect(),
3219 )
3220 }
3221 request => {
3222 return std::future::ready(Err(format!(
3223 "unexpected inventory request: {request:?}"
3224 )
3225 .into()));
3226 }
3227 };
3228 std::future::ready(Ok(response))
3229 }
3230 }
3231
3232 #[derive(Clone, Debug)]
3233 struct NormalNetworkResponder {
3234 block: Arc<Block>,
3235 transaction: UnminedTx,
3236 pushed_tx: tokio::sync::mpsc::UnboundedSender<UnminedTxId>,
3237 }
3238
3239 impl NormalNetworkResponder {
3240 fn header(&self) -> block::CountedHeader {
3241 block::CountedHeader {
3242 header: self.block.header.clone(),
3243 }
3244 }
3245 }
3246
3247 impl Service<Request> for NormalNetworkResponder {
3248 type Response = Response;
3249 type Error = BoxError;
3250 type Future = std::future::Ready<Result<Response, BoxError>>;
3251
3252 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
3253 Poll::Ready(Ok(()))
3254 }
3255
3256 fn call(&mut self, request: Request) -> Self::Future {
3257 let response = match request {
3258 Request::FindBlocks { .. } => Response::BlockHashes(vec![self.block.hash()]),
3259 Request::FindHeaders { .. } => Response::BlockHeaders(vec![self.header()]),
3260 Request::MempoolTransactionIds => {
3261 Response::TransactionIds(vec![self.transaction.id])
3262 }
3263 Request::BlocksByHash(hashes) | Request::BlocksByHashFrom { hashes, .. } => {
3264 Response::Blocks(
3265 hashes
3266 .into_iter()
3267 .map(|hash| {
3268 if hash == self.block.hash() {
3269 InventoryResponse::Available((self.block.clone(), None))
3270 } else {
3271 InventoryResponse::Missing(hash)
3272 }
3273 })
3274 .collect(),
3275 )
3276 }
3277 Request::PushTransaction(transaction, _) => {
3278 if let Err(error) = self.pushed_tx.send(transaction.id) {
3279 return std::future::ready(Err(Box::new(error)));
3280 }
3281 Response::Nil
3282 }
3283 request => {
3284 return std::future::ready(Err(format!(
3285 "unexpected normal-network request: {request:?}"
3286 )
3287 .into()));
3288 }
3289 };
3290 std::future::ready(Ok(response))
3291 }
3292 }
3293
3294 async fn legacy_node(
3295 seed: u64,
3296 ) -> Result<(ZakuraTestNode, UnboundedReceiver<Request>), BoxError> {
3297 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
3298 let node = ZakuraTestNode::builder(seed)
3299 .service_from_supervisor(move |supervisor| {
3300 Arc::new(LegacyGossipSink::spawn(RequestRecorder { tx }, supervisor))
3301 })
3302 .spawn()
3303 .await?;
3304 Ok((node, rx))
3305 }
3306
3307 async fn inventory_node(seed: u64, transaction: UnminedTx) -> Result<ZakuraTestNode, BoxError> {
3308 let node = ZakuraTestNode::builder(seed)
3309 .service_from_supervisor(move |supervisor| {
3310 Arc::new(LegacyGossipSink::spawn(
3311 InventoryResponder { transaction },
3312 supervisor,
3313 ))
3314 })
3315 .spawn()
3316 .await?;
3317 Ok(node)
3318 }
3319
3320 async fn block_inventory_node(
3321 seed: u64,
3322 block: Arc<Block>,
3323 ) -> Result<ZakuraTestNode, BoxError> {
3324 let node = ZakuraTestNode::builder(seed)
3325 .service_from_supervisor(move |supervisor| {
3326 Arc::new(LegacyGossipSink::spawn(
3327 BlockInventoryResponder { block },
3328 supervisor,
3329 ))
3330 })
3331 .spawn()
3332 .await?;
3333 Ok(node)
3334 }
3335
3336 async fn recording_inventory_node(
3337 seed: u64,
3338 transaction: Option<UnminedTx>,
3339 ) -> Result<(ZakuraTestNode, UnboundedReceiver<Request>), BoxError> {
3340 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
3341 let node = ZakuraTestNode::builder(seed)
3342 .service_from_supervisor(move |supervisor| {
3343 Arc::new(LegacyGossipSink::spawn(
3344 RecordingInventoryResponder { transaction, tx },
3345 supervisor,
3346 ))
3347 })
3348 .spawn()
3349 .await?;
3350 Ok((node, rx))
3351 }
3352
3353 async fn normal_network_node(
3354 seed: u64,
3355 block: Arc<Block>,
3356 transaction: UnminedTx,
3357 ) -> Result<
3358 (
3359 ZakuraTestNode,
3360 tokio::sync::mpsc::UnboundedReceiver<UnminedTxId>,
3361 ),
3362 BoxError,
3363 > {
3364 let (pushed_tx, pushed_rx) = tokio::sync::mpsc::unbounded_channel();
3365 let node = ZakuraTestNode::builder(seed)
3366 .max_connections_per_ip(8)
3371 .service_from_supervisor(move |supervisor| {
3372 Arc::new(LegacyGossipSink::spawn(
3373 NormalNetworkResponder {
3374 block,
3375 transaction,
3376 pushed_tx,
3377 },
3378 supervisor,
3379 ))
3380 })
3381 .spawn()
3382 .await?;
3383 Ok((node, pushed_rx))
3384 }
3385
3386 async fn node_peer_id(node: &ZakuraTestNode) -> Result<ZakuraPeerId, BoxError> {
3387 Ok(ZakuraPeerId::new(
3388 node.node_addr().await.node_id.as_bytes().to_vec(),
3389 )?)
3390 }
3391
3392 async fn recv_request(rx: &mut UnboundedReceiver<Request>) -> Result<Request, BoxError> {
3393 tokio::time::timeout(TEST_NET_TIMEOUT, rx.recv())
3394 .await
3395 .map_err(|_| -> BoxError { "timed out waiting for request".into() })?
3396 .ok_or_else(|| "request recorder closed".into())
3397 }
3398
3399 async fn recv_pushed_tx_id(
3400 rx: &mut tokio::sync::mpsc::UnboundedReceiver<UnminedTxId>,
3401 ) -> Result<UnminedTxId, BoxError> {
3402 tokio::time::timeout(TEST_NET_TIMEOUT, rx.recv())
3403 .await
3404 .map_err(|_| -> BoxError { "timed out waiting for pushed transaction".into() })?
3405 .ok_or_else(|| "pushed transaction recorder closed".into())
3406 }
3407
3408 fn legacy_gossip_peer(
3409 peer_id: ZakuraPeerId,
3410 cancel_token: tokio_util::sync::CancellationToken,
3411 ) -> (Peer, FramedSend) {
3412 legacy_gossip_peer_with_conn(peer_id, 0, cancel_token)
3413 }
3414
3415 fn legacy_gossip_peer_with_conn(
3416 peer_id: ZakuraPeerId,
3417 conn_id: ZakuraConnId,
3418 cancel_token: tokio_util::sync::CancellationToken,
3419 ) -> (Peer, FramedSend) {
3420 let (peer_send, service_recv) = framed_channel(8);
3421 let (service_send, _peer_recv) = framed_channel(8);
3422 let peer = Peer::new_with_conn_id_and_direction(
3423 conn_id,
3424 peer_id,
3425 None,
3426 ZAKURA_CAP_LEGACY_GOSSIP,
3427 ServicePeerDirection::Inbound,
3428 HashMap::from([(ZAKURA_STREAM_GOSSIP, (service_recv, service_send))]),
3429 cancel_token,
3430 );
3431 (peer, peer_send)
3432 }
3433
3434 async fn wait_for_legacy_gossip_panic_cleanup(
3435 outbound: &LegacyGossipOutbound,
3436 peer_id: &ZakuraPeerId,
3437 cancel_token: &tokio_util::sync::CancellationToken,
3438 ) -> Result<(), BoxError> {
3439 tokio::time::timeout(Duration::from_secs(1), async {
3440 loop {
3441 if cancel_token.is_cancelled() && !outbound.contains(peer_id) {
3442 return;
3443 }
3444 tokio::time::sleep(Duration::from_millis(10)).await;
3445 }
3446 })
3447 .await
3448 .map_err(|_| -> BoxError { "timed out waiting for legacy gossip panic cleanup".into() })
3449 }
3450
3451 async fn wait_registered_count(node: &ZakuraTestNode, count: usize) -> Result<(), BoxError> {
3452 tokio::time::timeout(TEST_NET_TIMEOUT, async {
3453 loop {
3454 if node.supervisor().registered_ids().await.len() == count {
3455 return;
3456 }
3457 tokio::time::sleep(Duration::from_millis(10)).await;
3458 }
3459 })
3460 .await
3461 .map_err(|_| -> BoxError { "timed out waiting for peer registration count".into() })
3462 }
3463
3464 #[tokio::test(start_paused = true)]
3474 async fn legacy_request_handler_releases_permit_when_request_stream_drops_receiver() {
3475 let permits = Arc::new(Semaphore::new(LEGACY_REQUEST_IN_FLIGHT_LIMIT));
3476 let permit = permits
3477 .clone()
3478 .try_acquire_owned()
3479 .expect("a permit is available");
3480 assert_eq!(
3481 permits.available_permits(),
3482 LEGACY_REQUEST_IN_FLIGHT_LIMIT - 1,
3483 "one permit is held while the handler runs"
3484 );
3485
3486 let (response_tx, response_rx) = oneshot::channel();
3487 let request = LegacyRequestInbound {
3488 peer_id: ZakuraPeerId::new(vec![7u8; 32]).expect("valid peer id"),
3489 request_id: 1,
3490 frame: LegacyRequestFrame::MempoolTransactionIds,
3492 response_tx,
3493 };
3494
3495 let handler = tokio::spawn(handle_legacy_request(
3496 NeverCompletesService,
3497 request,
3498 permit,
3499 ZakuraTrace::noop(),
3500 ));
3501
3502 tokio::task::yield_now().await;
3505 drop(response_rx);
3506
3507 tokio::time::timeout(TEST_NET_TIMEOUT, handler)
3511 .await
3512 .expect("handler aborts promptly after the request stream drops the receiver")
3513 .expect("handler task does not panic");
3514
3515 assert_eq!(
3516 permits.available_permits(),
3517 LEGACY_REQUEST_IN_FLIGHT_LIMIT,
3518 "the in-flight permit must be released once the handler aborts"
3519 );
3520 }
3521
3522 #[test]
3523 fn block_gossip_round_trips_and_rejects_trailing_bytes() {
3524 let frame = LegacyGossipFrame::AdvertiseBlock(block_hash(1));
3525 let mut encoded = frame.encode_frame().expect("frame encodes");
3526 assert_eq!(
3527 LegacyGossipFrame::decode_frame(encoded.clone()).expect("frame decodes"),
3528 frame
3529 );
3530
3531 encoded.payload.push(0);
3532 assert!(matches!(
3533 LegacyGossipFrame::decode_frame(encoded),
3534 Err(LegacyGossipError::TrailingBytes)
3535 ));
3536 }
3537
3538 #[tokio::test]
3539 async fn adapter_gossips_block_and_tx_ids_between_two_nodes() -> Result<(), BoxError> {
3540 let _guard = zakura_test::init();
3541 let (node_a, mut rx_a) = legacy_node(11).await?;
3542 let (node_b, mut rx_b) = legacy_node(12).await?;
3543 node_a.connect_native(&node_b, TEST_NET_TIMEOUT).await?;
3544 let a_peer_id = node_peer_id(&node_a).await?;
3545
3546 let mut adapter = LegacyGossipAdapter::new(node_a.supervisor());
3547 let block_hash = block_hash(9);
3548 adapter
3549 .ready()
3550 .await?
3551 .call(Request::AdvertiseBlockToAll(block_hash))
3552 .await?;
3553
3554 match recv_request(&mut rx_b).await? {
3555 Request::AdvertiseBlock(hash, Some(PeerSource::Zakura(peer_id))) => {
3556 assert_eq!(hash, block_hash);
3557 assert_eq!(peer_id, a_peer_id);
3558 }
3559 request => panic!("unexpected request: {request:?}"),
3560 }
3561 assert!(rx_a.try_recv().is_err());
3562
3563 let ids = HashSet::from([legacy_tx_id(10), witnessed_tx_id(11)]);
3564 adapter
3565 .ready()
3566 .await?
3567 .call(Request::AdvertiseTransactionIds(ids.clone(), None))
3568 .await?;
3569
3570 match recv_request(&mut rx_b).await? {
3571 Request::AdvertiseTransactionIds(received, Some(PeerSource::Zakura(peer_id))) => {
3572 assert_eq!(received, ids);
3573 assert_eq!(peer_id, a_peer_id);
3574 }
3575 request => panic!("unexpected request: {request:?}"),
3576 }
3577
3578 node_a.shutdown().await;
3579 node_b.shutdown().await;
3580 Ok(())
3581 }
3582
3583 #[tokio::test]
3584 async fn request_adapter_fetches_missing_block_from_advertiser() -> Result<(), BoxError> {
3585 let _guard = zakura_test::init();
3586 let transaction = UnminedTx::from(empty_v5_transaction(1));
3587 let node_a = inventory_node(61, transaction).await?;
3588 let node_b = ZakuraTestNode::builder(62).spawn().await?;
3589 node_b.connect_native(&node_a, TEST_NET_TIMEOUT).await?;
3590 let a_peer_id = node_peer_id(&node_a).await?;
3591 let hash = block_hash(90);
3592
3593 let adapter = LegacyRequestAdapter::new(node_b.supervisor());
3594 let response = adapter
3595 .request_from_source(
3596 Request::BlocksByHash(HashSet::from([hash])),
3597 Some(PeerSource::Zakura(a_peer_id)),
3598 )
3599 .await?;
3600
3601 match response {
3602 Response::Blocks(blocks) => {
3603 assert_eq!(blocks, vec![InventoryResponse::Missing(hash)]);
3604 }
3605 response => panic!("unexpected response: {response:?}"),
3606 }
3607
3608 node_a.shutdown().await;
3609 node_b.shutdown().await;
3610 Ok(())
3611 }
3612
3613 #[tokio::test]
3614 async fn request_adapter_fetches_available_multi_chunk_block() -> Result<(), BoxError> {
3615 let _guard = zakura_test::init();
3616 let block = Arc::new(Block::zcash_deserialize(
3617 BLOCK_TESTNET_141042_BYTES.as_slice(),
3618 )?);
3619 let serialized_len = block.zcash_serialize_to_vec()?.len();
3620 assert!(
3621 serialized_len > LEGACY_RESPONSE_CHUNK_BYTES,
3622 "test block must span multiple response chunks"
3623 );
3624
3625 let node_a = block_inventory_node(68, block.clone()).await?;
3626 let node_b = ZakuraTestNode::builder(69).spawn().await?;
3627 node_b.connect_native(&node_a, TEST_NET_TIMEOUT).await?;
3628 let a_peer_id = node_peer_id(&node_a).await?;
3629
3630 let adapter = LegacyRequestAdapter::new(node_b.supervisor());
3631 let response = adapter
3632 .request_from_source(
3633 Request::BlocksByHash(HashSet::from([block.hash()])),
3634 Some(PeerSource::Zakura(a_peer_id)),
3635 )
3636 .await?;
3637
3638 let Response::Blocks(blocks) = response else {
3639 panic!("unexpected response: {response:?}");
3640 };
3641 assert!(matches!(
3642 blocks.as_slice(),
3643 [InventoryResponse::Available((received, None))] if received.hash() == block.hash()
3644 ));
3645
3646 node_a.shutdown().await;
3647 node_b.shutdown().await;
3648 Ok(())
3649 }
3650
3651 #[tokio::test]
3652 async fn request_adapter_fetches_available_and_missing_transactions() -> Result<(), BoxError> {
3653 let _guard = zakura_test::init();
3654 let transaction = UnminedTx::from(empty_v5_transaction(2));
3655 let available_id = transaction.id;
3656 let missing_id = witnessed_tx_id(99);
3657 let node_a = inventory_node(63, transaction.clone()).await?;
3658 let node_b = ZakuraTestNode::builder(64).spawn().await?;
3659 node_b.connect_native(&node_a, TEST_NET_TIMEOUT).await?;
3660 let a_peer_id = node_peer_id(&node_a).await?;
3661
3662 let adapter = LegacyRequestAdapter::new(node_b.supervisor());
3663 let response = adapter
3664 .request_from_source(
3665 Request::TransactionsById(HashSet::from([available_id, missing_id])),
3666 Some(PeerSource::Zakura(a_peer_id)),
3667 )
3668 .await?;
3669
3670 let Response::Transactions(transactions) = response else {
3671 panic!("unexpected response: {response:?}");
3672 };
3673 assert!(transactions.iter().any(|response| {
3674 matches!(
3675 response,
3676 InventoryResponse::Available((tx, None)) if tx.id == transaction.id
3677 )
3678 }));
3679 assert!(transactions.iter().any(
3680 |response| matches!(response, InventoryResponse::Missing(id) if *id == missing_id)
3681 ));
3682
3683 node_a.shutdown().await;
3684 node_b.shutdown().await;
3685 Ok(())
3686 }
3687
3688 #[tokio::test]
3689 async fn request_adapter_drives_mock_chain_sync_progress_over_zakura() -> Result<(), BoxError> {
3690 let _guard = zakura_test::init();
3691 let block = Arc::new(Block::zcash_deserialize(
3692 BLOCK_TESTNET_141042_BYTES.as_slice(),
3693 )?);
3694 let transaction = UnminedTx::from(empty_v5_transaction(4));
3695 let (node_a, _pushed_rx) = normal_network_node(81, block.clone(), transaction).await?;
3696 let node_b = ZakuraTestNode::builder(82).spawn().await?;
3697 node_b.connect_native(&node_a, TEST_NET_TIMEOUT).await?;
3698 let a_peer_id = node_peer_id(&node_a).await?;
3699
3700 let adapter = LegacyRequestAdapter::new(node_b.supervisor());
3701 let mut local_chain = vec![block_hash(1)];
3702 let find_headers = adapter
3703 .request_from_source(
3704 Request::FindHeaders {
3705 known_blocks: local_chain.clone(),
3706 stop: Some(block.hash()),
3707 },
3708 Some(PeerSource::Zakura(a_peer_id.clone())),
3709 )
3710 .await?;
3711 let Response::BlockHeaders(headers) = find_headers else {
3712 panic!("unexpected FindHeaders response: {find_headers:?}");
3713 };
3714 assert!(matches!(
3715 headers.as_slice(),
3716 [header] if header.header == block.header
3717 ));
3718
3719 let find_blocks = adapter
3720 .request_from_source(
3721 Request::FindBlocks {
3722 known_blocks: local_chain.clone(),
3723 stop: Some(block.hash()),
3724 },
3725 Some(PeerSource::Zakura(a_peer_id.clone())),
3726 )
3727 .await?;
3728 let Response::BlockHashes(remote_hashes) = find_blocks else {
3729 panic!("unexpected FindBlocks response: {find_blocks:?}");
3730 };
3731 assert_eq!(remote_hashes, vec![block.hash()]);
3732
3733 let block_response = adapter
3734 .request_from_source(
3735 Request::BlocksByHashFrom {
3736 hashes: remote_hashes.into_iter().collect(),
3737 source: PeerSource::Zakura(a_peer_id),
3738 },
3739 None,
3740 )
3741 .await?;
3742 let Response::Blocks(blocks) = block_response else {
3743 panic!("unexpected block response: {block_response:?}");
3744 };
3745 for response in blocks {
3746 let received = response
3747 .available()
3748 .expect("mock sync peer serves the advertised block")
3749 .0;
3750 local_chain.push(received.hash());
3751 }
3752 assert_eq!(local_chain.last(), Some(&block.hash()));
3753 assert_eq!(local_chain.len(), 2);
3754
3755 node_a.shutdown().await;
3756 node_b.shutdown().await;
3757 Ok(())
3758 }
3759
3760 #[tokio::test]
3761 async fn controlled_three_node_network_gossips_fetches_and_advances_mock_chain(
3762 ) -> Result<(), BoxError> {
3763 let _guard = zakura_test::init();
3764 let block = Arc::new(Block::zcash_deserialize(
3765 BLOCK_TESTNET_141042_BYTES.as_slice(),
3766 )?);
3767 let transaction = UnminedTx::from(empty_v5_transaction(14));
3768 let (node_a, _pushed_rx) = normal_network_node(85, block.clone(), transaction).await?;
3769 let (node_b, mut rx_b) = legacy_node(86).await?;
3770 let (node_c, mut rx_c) = legacy_node(87).await?;
3771
3772 node_a.connect_native(&node_b, TEST_NET_TIMEOUT).await?;
3773 node_a.connect_native(&node_c, TEST_NET_TIMEOUT).await?;
3774 node_b.connect_native(&node_c, TEST_NET_TIMEOUT).await?;
3775 wait_registered_count(&node_a, 2).await?;
3776 wait_registered_count(&node_b, 2).await?;
3777 wait_registered_count(&node_c, 2).await?;
3778
3779 let a_peer_id = node_peer_id(&node_a).await?;
3780 let mut gossip = LegacyGossipAdapter::new(node_a.supervisor());
3781 gossip
3782 .ready()
3783 .await?
3784 .call(Request::AdvertiseBlockToAll(block.hash()))
3785 .await?;
3786
3787 match recv_request(&mut rx_b).await? {
3788 Request::AdvertiseBlock(hash, Some(PeerSource::Zakura(peer_id))) => {
3789 assert_eq!(hash, block.hash());
3790 assert_eq!(peer_id, a_peer_id);
3791 }
3792 request => panic!("unexpected B request: {request:?}"),
3793 }
3794 match recv_request(&mut rx_c).await? {
3795 Request::AdvertiseBlock(hash, Some(PeerSource::Zakura(_))) => {
3796 assert_eq!(hash, block.hash());
3797 }
3798 request => panic!("unexpected C request: {request:?}"),
3799 }
3800
3801 let adapter = LegacyRequestAdapter::new(node_c.supervisor());
3802 let mut local_chain = vec![block_hash(1)];
3803 let headers = adapter
3804 .request_from_source(
3805 Request::FindHeaders {
3806 known_blocks: local_chain.clone(),
3807 stop: Some(block.hash()),
3808 },
3809 Some(PeerSource::Zakura(a_peer_id.clone())),
3810 )
3811 .await?;
3812 assert!(matches!(
3813 headers,
3814 Response::BlockHeaders(headers) if matches!(
3815 headers.as_slice(),
3816 [header] if header.header == block.header
3817 )
3818 ));
3819
3820 let hashes = adapter
3821 .request_from_source(
3822 Request::FindBlocks {
3823 known_blocks: local_chain.clone(),
3824 stop: Some(block.hash()),
3825 },
3826 Some(PeerSource::Zakura(a_peer_id.clone())),
3827 )
3828 .await?;
3829 let Response::BlockHashes(hashes) = hashes else {
3830 panic!("unexpected FindBlocks response: {hashes:?}");
3831 };
3832 assert_eq!(hashes, vec![block.hash()]);
3833
3834 let blocks = adapter
3835 .request_from_source(
3836 Request::BlocksByHashFrom {
3837 hashes: hashes.into_iter().collect(),
3838 source: PeerSource::Zakura(a_peer_id),
3839 },
3840 None,
3841 )
3842 .await?;
3843 let Response::Blocks(blocks) = blocks else {
3844 panic!("unexpected block response: {blocks:?}");
3845 };
3846 for response in blocks {
3847 let received = response
3848 .available()
3849 .expect("mock sync peer serves the gossiped block")
3850 .0;
3851 local_chain.push(received.hash());
3852 }
3853 assert_eq!(local_chain.last(), Some(&block.hash()));
3854 assert_eq!(local_chain.len(), 2);
3855
3856 node_a.shutdown().await;
3857 node_b.shutdown().await;
3858 node_c.shutdown().await;
3859 Ok(())
3860 }
3861
3862 #[tokio::test]
3863 async fn request_adapter_supports_mempool_ping_and_push_transaction() -> Result<(), BoxError> {
3864 let _guard = zakura_test::init();
3865 let block = Arc::new(Block::zcash_deserialize(
3866 BLOCK_TESTNET_141042_BYTES.as_slice(),
3867 )?);
3868 let transaction = UnminedTx::from(empty_v5_transaction(5));
3869 let pushed_transaction = UnminedTx::from(empty_v5_transaction(6));
3870 let pushed_id = pushed_transaction.id;
3871 let (node_a, mut pushed_rx) = normal_network_node(83, block, transaction.clone()).await?;
3872 let node_b = ZakuraTestNode::builder(84).spawn().await?;
3873 node_b.connect_native(&node_a, TEST_NET_TIMEOUT).await?;
3874 let a_peer_id = node_peer_id(&node_a).await?;
3875
3876 let adapter = LegacyRequestAdapter::new(node_b.supervisor());
3877 let mempool = adapter
3878 .request_from_source(
3879 Request::MempoolTransactionIds,
3880 Some(PeerSource::Zakura(a_peer_id.clone())),
3881 )
3882 .await?;
3883 assert_eq!(mempool, Response::TransactionIds(vec![transaction.id]));
3884
3885 let ping = adapter
3886 .request_from_source(
3887 Request::Ping(Default::default()),
3888 Some(PeerSource::Zakura(a_peer_id)),
3889 )
3890 .await?;
3891 assert!(matches!(ping, Response::Pong(_)));
3892
3893 let push_response = adapter
3894 .request_from_source(Request::PushTransaction(pushed_transaction, None), None)
3895 .await?;
3896 assert_eq!(push_response, Response::Nil);
3897 assert_eq!(recv_pushed_tx_id(&mut pushed_rx).await?, pushed_id);
3898
3899 node_a.shutdown().await;
3900 node_b.shutdown().await;
3901 Ok(())
3902 }
3903
3904 #[tokio::test]
3905 async fn slow_request_does_not_block_gossip_worker() -> Result<(), BoxError> {
3906 let (request_tx, request_rx) = oneshot::channel();
3907 let (gossip_tx, mut gossip_rx) = tokio::sync::mpsc::unbounded_channel();
3908 let (release_tx, release_rx) = tokio::sync::watch::channel(false);
3909 let (inbound_tx, inbound_rx) = mpsc::channel(4);
3910 let supervisor = ZakuraSupervisorHandle::new(1);
3911 let peer_id = ZakuraPeerId::new(vec![6; 32]).expect("test peer id is within bounds");
3912
3913 let worker = tokio::spawn(legacy_gossip_worker(
3914 SlowRequestThenRecorder {
3915 release: release_rx,
3916 tx: gossip_tx,
3917 },
3918 inbound_rx,
3919 LegacyGossipForwarder::new(supervisor),
3920 ZakuraTrace::noop(),
3921 ));
3922
3923 inbound_tx
3924 .send(LegacyInboundWork::Request(LegacyRequestInbound {
3925 peer_id: peer_id.clone(),
3926 request_id: 1,
3927 frame: LegacyRequestFrame::BlocksByHash(vec![block_hash(11)]),
3928 response_tx: request_tx,
3929 }))
3930 .await?;
3931 inbound_tx
3932 .send(LegacyInboundWork::Gossip(LegacyGossipInbound {
3933 peer_id: peer_id.clone(),
3934 frame: LegacyGossipFrame::AdvertiseBlock(block_hash(12)),
3935 }))
3936 .await?;
3937
3938 match recv_request(&mut gossip_rx).await? {
3939 Request::AdvertiseBlock(hash, Some(PeerSource::Zakura(source))) => {
3940 assert_eq!(hash, block_hash(12));
3941 assert_eq!(source, peer_id);
3942 }
3943 request => panic!("unexpected request: {request:?}"),
3944 }
3945
3946 release_tx.send(true)?;
3947 match tokio::time::timeout(Duration::from_secs(1), request_rx).await??? {
3948 Response::Blocks(blocks) => {
3949 assert_eq!(blocks, vec![InventoryResponse::Missing(block_hash(11))]);
3950 }
3951 response => panic!("unexpected request response: {response:?}"),
3952 }
3953
3954 drop(inbound_tx);
3955 worker.await?;
3956 Ok(())
3957 }
3958
3959 #[tokio::test(start_paused = true)]
3968 async fn duplicate_gossip_does_not_repeat_expensive_attempts_while_service_is_slow(
3969 ) -> Result<(), BoxError> {
3970 let calls = Arc::new(AtomicUsize::new(0));
3971 let (inbound_tx, inbound_rx) = mpsc::channel(16);
3972 let supervisor = ZakuraSupervisorHandle::new(1);
3973 let peer_id = ZakuraPeerId::new(vec![7; 32]).expect("test peer id is within bounds");
3974
3975 let worker = tokio::spawn(legacy_gossip_worker(
3976 CountingPendingService {
3977 calls: calls.clone(),
3978 },
3979 inbound_rx,
3980 LegacyGossipForwarder::new(supervisor),
3981 ZakuraTrace::noop(),
3982 ));
3983
3984 let frame = LegacyGossipFrame::AdvertiseBlock(block_hash(99));
3987 for _ in 0..4 {
3988 inbound_tx
3989 .send(LegacyInboundWork::Gossip(LegacyGossipInbound {
3990 peer_id: peer_id.clone(),
3991 frame: frame.clone(),
3992 }))
3993 .await?;
3994 }
3995
3996 drop(inbound_tx);
3997 worker.await?;
3998
3999 assert_eq!(
4000 calls.load(Ordering::SeqCst),
4001 1,
4002 "duplicate gossip frames must not each re-pay the inbound readiness/call \
4003 timeout while the service is slow; expected one expensive attempt with \
4004 the rest suppressed by the cooldown"
4005 );
4006
4007 Ok(())
4008 }
4009
4010 #[tokio::test]
4011 async fn service_request_routes_to_advertiser_then_fallback_after_missing(
4012 ) -> Result<(), BoxError> {
4013 let _guard = zakura_test::init();
4014 let transaction = UnminedTx::from(empty_v5_transaction(3));
4015 let txid = transaction.id;
4016 let (advertiser, mut advertiser_rx) = recording_inventory_node(65, None).await?;
4017 let (fallback, mut fallback_rx) =
4018 recording_inventory_node(66, Some(transaction.clone())).await?;
4019 let requester = ZakuraTestNode::builder(67)
4023 .max_connections_per_ip(8)
4024 .spawn()
4025 .await?;
4026 requester
4027 .connect_native(&advertiser, TEST_NET_TIMEOUT)
4028 .await?;
4029 requester
4030 .connect_native(&fallback, TEST_NET_TIMEOUT)
4031 .await?;
4032 wait_registered_count(&requester, 2).await?;
4033 let advertiser_id = node_peer_id(&advertiser).await?;
4034
4035 let mut adapter = LegacyRequestAdapter::new(requester.supervisor());
4036 let response = adapter
4037 .ready()
4038 .await?
4039 .call(Request::TransactionsByIdFrom {
4040 ids: HashSet::from([txid]),
4041 source: PeerSource::Zakura(advertiser_id),
4042 })
4043 .await?;
4044
4045 match recv_request(&mut advertiser_rx).await? {
4046 Request::TransactionsById(ids) => assert_eq!(ids, HashSet::from([txid])),
4047 request => panic!("unexpected advertiser request: {request:?}"),
4048 }
4049 match recv_request(&mut fallback_rx).await? {
4050 Request::TransactionsById(ids) => assert_eq!(ids, HashSet::from([txid])),
4051 request => panic!("unexpected fallback request: {request:?}"),
4052 }
4053
4054 let Response::Transactions(transactions) = response else {
4055 panic!("unexpected response: {response:?}");
4056 };
4057 assert!(matches!(
4058 transactions.as_slice(),
4059 [InventoryResponse::Available((tx, None))] if tx.id == transaction.id
4060 ));
4061
4062 advertiser.shutdown().await;
4063 fallback.shutdown().await;
4064 requester.shutdown().await;
4065 Ok(())
4066 }
4067
4068 #[tokio::test]
4069 async fn first_seen_gossip_forwards_across_line_and_drops_duplicates() -> Result<(), BoxError> {
4070 let _guard = zakura_test::init();
4071 let (node_a, mut rx_a) = legacy_node(21).await?;
4072 let (node_b, mut rx_b) = legacy_node(22).await?;
4073 let (node_c, mut rx_c) = legacy_node(23).await?;
4074
4075 node_a.connect_native(&node_b, TEST_NET_TIMEOUT).await?;
4076 node_b.connect_native(&node_c, TEST_NET_TIMEOUT).await?;
4077
4078 wait_registered_count(&node_b, 2).await?;
4079
4080 let a_peer_id = node_peer_id(&node_a).await?;
4081 let b_peer_id = node_peer_id(&node_b).await?;
4082 let block_hash = block_hash(42);
4083 let mut adapter = LegacyGossipAdapter::new(node_a.supervisor());
4084 adapter
4085 .ready()
4086 .await?
4087 .call(Request::AdvertiseBlockToAll(block_hash))
4088 .await?;
4089
4090 match recv_request(&mut rx_b).await? {
4091 Request::AdvertiseBlock(hash, Some(PeerSource::Zakura(peer_id))) => {
4092 assert_eq!(hash, block_hash);
4093 assert_eq!(peer_id, a_peer_id);
4094 }
4095 request => panic!("unexpected B request: {request:?}"),
4096 }
4097 match recv_request(&mut rx_c).await? {
4098 Request::AdvertiseBlock(hash, Some(PeerSource::Zakura(peer_id))) => {
4099 assert_eq!(hash, block_hash);
4100 assert_eq!(peer_id, b_peer_id);
4101 }
4102 request => panic!("unexpected C request: {request:?}"),
4103 }
4104 assert!(rx_a.try_recv().is_err());
4105
4106 adapter
4107 .ready()
4108 .await?
4109 .call(Request::AdvertiseBlockToAll(block_hash))
4110 .await?;
4111 tokio::time::sleep(Duration::from_millis(100)).await;
4112 assert!(rx_c.try_recv().is_err());
4113
4114 node_a.shutdown().await;
4115 node_b.shutdown().await;
4116 node_c.shutdown().await;
4117 Ok(())
4118 }
4119
4120 #[tokio::test]
4121 async fn local_origin_echo_is_dropped_by_shared_first_seen_cache() -> Result<(), BoxError> {
4122 let _guard = zakura_test::init();
4123 let (node_a, mut rx_a) = legacy_node(51).await?;
4124 let (node_b, mut rx_b) = legacy_node(52).await?;
4125 node_a.connect_native(&node_b, TEST_NET_TIMEOUT).await?;
4126 let hostile = HostilePeer::connect_native(&node_a, 53).await?;
4127 wait_registered_count(&node_a, 2).await?;
4128
4129 let block_hash = block_hash(88);
4130 let mut adapter = LegacyGossipAdapter::new(node_a.supervisor());
4131 adapter
4132 .ready()
4133 .await?
4134 .call(Request::AdvertiseBlockToAll(block_hash))
4135 .await?;
4136 let _ = recv_request(&mut rx_b).await?;
4137
4138 let echo_payload = LegacyGossipFrame::AdvertiseBlock(block_hash)
4139 .encode_frame()?
4140 .payload;
4141 hostile
4142 .send_frame(ZAKURA_STREAM_GOSSIP, echo_payload)
4143 .await?;
4144 tokio::time::sleep(Duration::from_millis(100)).await;
4145
4146 assert!(
4147 rx_a.try_recv().is_err(),
4148 "origin must not accept its own echo"
4149 );
4150 assert!(
4151 rx_b.try_recv().is_err(),
4152 "origin must not re-forward its own echo"
4153 );
4154
4155 hostile.shutdown().await;
4156 node_a.shutdown().await;
4157 node_b.shutdown().await;
4158 Ok(())
4159 }
4160
4161 #[tokio::test]
4162 async fn saturated_peer_does_not_block_honest_fanout() -> Result<(), BoxError> {
4163 let saturated_peer = ZakuraPeerId::new(vec![1; 32]).expect("test peer id is within bounds");
4164 let honest_peer = ZakuraPeerId::new(vec![2; 32]).expect("test peer id is within bounds");
4165 let (saturated, _saturated_rx) = framed_channel(1);
4166 saturated.try_send(Frame {
4167 message_type: MSG_ADVERTISE_BLOCK,
4168 flags: 0,
4169 payload: vec![1],
4170 })?;
4171
4172 let (honest, mut honest_rx) = framed_channel(1);
4173 let block_hash = block_hash(7);
4174
4175 let result = send_to_sessions(
4176 vec![
4177 LegacyGossipPeerSession::new(saturated_peer, 0, saturated),
4178 LegacyGossipPeerSession::new(honest_peer, 0, honest),
4179 ],
4180 LegacyGossipFrame::AdvertiseBlock(block_hash),
4181 );
4182 let outbound = tokio::time::timeout(Duration::from_secs(1), honest_rx.recv())
4183 .await?
4184 .expect("honest peer receives fanout");
4185 assert_eq!(
4186 LegacyGossipFrame::decode_frame(outbound)?,
4187 LegacyGossipFrame::AdvertiseBlock(block_hash)
4188 );
4189
4190 assert!(
4191 result.is_err(),
4192 "saturated peer failure is reported after honest peer is attempted"
4193 );
4194 Ok(())
4195 }
4196
4197 #[tokio::test]
4198 async fn new_peer_receives_latest_block_advertised_before_gossip_stream_ready(
4199 ) -> Result<(), BoxError> {
4200 let supervisor = ZakuraSupervisorHandle::new(991);
4201 let broadcast = ZakuraGossipBroadcast::new(supervisor);
4202 let block_hash = block_hash(91);
4203
4204 broadcast
4205 .broadcast(LegacyGossipFrame::AdvertiseBlock(block_hash), None)
4206 .await?;
4207
4208 let peer_id = ZakuraPeerId::new(vec![91; 32]).expect("test peer id is within bounds");
4209 let (sender, mut receiver) = framed_channel(1);
4210 let session = LegacyGossipPeerSession::new(peer_id, 0, sender);
4211 broadcast.outbound.insert(session.clone());
4212 broadcast
4213 .outbound
4214 .replay_latest_block_to_peer(session)
4215 .await?;
4216
4217 let replayed = tokio::time::timeout(Duration::from_secs(1), receiver.recv())
4218 .await?
4219 .expect("new peer receives latest block replay");
4220 assert_eq!(
4221 LegacyGossipFrame::decode_frame(replayed)?,
4222 LegacyGossipFrame::AdvertiseBlock(block_hash)
4223 );
4224 Ok(())
4225 }
4226
4227 #[tokio::test]
4228 async fn legacy_gossip_replay_panic_cancels_peer_and_removes_outbound_session(
4229 ) -> Result<(), BoxError> {
4230 let (inbound_tx, _inbound_rx) = mpsc::channel(1);
4231 let outbound = LegacyGossipOutbound::default();
4232 let sink = LegacyGossipSink {
4233 inbound_tx,
4234 outbound: outbound.clone(),
4235 trace: ZakuraTrace::noop(),
4236 };
4237 let peer_id = ZakuraPeerId::new(vec![92; 32]).expect("test peer id is within bounds");
4238 let cancel_token = tokio_util::sync::CancellationToken::new();
4239 let (peer, _peer_send) = legacy_gossip_peer(peer_id.clone(), cancel_token.clone());
4240
4241 let poisoned_latest_block = outbound.latest_block.clone();
4242 let _ = std::panic::catch_unwind(move || {
4243 let _guard = poisoned_latest_block
4244 .lock()
4245 .expect("latest-block mutex starts unpoisoned");
4246 panic!("poison latest-block mutex before replay");
4247 });
4248
4249 sink.add_peer(peer);
4250 wait_for_legacy_gossip_panic_cleanup(&outbound, &peer_id, &cancel_token).await
4251 }
4252
4253 #[tokio::test]
4254 async fn legacy_gossip_recv_loop_panic_cancels_peer_and_removes_outbound_session(
4255 ) -> Result<(), BoxError> {
4256 let (inbound_tx, _inbound_rx) = mpsc::channel(1);
4257 let outbound = LegacyGossipOutbound::default();
4258 let sink = LegacyGossipSink {
4259 inbound_tx,
4260 outbound: outbound.clone(),
4261 trace: ZakuraTrace::noop(),
4262 };
4263 let peer_id = ZakuraPeerId::new(vec![93; 32]).expect("test peer id is within bounds");
4264 let cancel_token = tokio_util::sync::CancellationToken::new();
4265 let (peer, peer_send) = legacy_gossip_peer(peer_id.clone(), cancel_token.clone());
4266
4267 arm_legacy_gossip_recv_loop_panic(peer_id.clone());
4268 sink.add_peer(peer);
4269 peer_send
4270 .send(LegacyGossipFrame::AdvertiseBlock(block_hash(93)).encode_frame()?)
4271 .await
4272 .map_err(|_| -> BoxError { "failed to send test gossip frame".into() })?;
4273
4274 wait_for_legacy_gossip_panic_cleanup(&outbound, &peer_id, &cancel_token).await
4275 }
4276
4277 #[tokio::test]
4278 async fn stale_legacy_gossip_teardown_keeps_replacement_session() {
4279 let (inbound_tx, _inbound_rx) = mpsc::channel(1);
4280 let outbound = LegacyGossipOutbound::default();
4281 let sink = LegacyGossipSink {
4282 inbound_tx,
4283 outbound: outbound.clone(),
4284 trace: ZakuraTrace::noop(),
4285 };
4286 let peer_id = ZakuraPeerId::new(vec![94; 32]).expect("test peer id is within bounds");
4287 let old_conn_id = 1;
4288 let new_conn_id = 2;
4289 let old_cancel = tokio_util::sync::CancellationToken::new();
4290 let new_cancel = tokio_util::sync::CancellationToken::new();
4291 let (old_peer, _old_peer_send) =
4292 legacy_gossip_peer_with_conn(peer_id.clone(), old_conn_id, old_cancel);
4293 let (new_peer, _new_peer_send) =
4294 legacy_gossip_peer_with_conn(peer_id.clone(), new_conn_id, new_cancel);
4295
4296 sink.add_peer(old_peer);
4297 assert!(
4298 outbound.contains(&peer_id),
4299 "old legacy gossip session is registered",
4300 );
4301 sink.add_peer(new_peer);
4302 assert!(
4303 outbound.contains(&peer_id),
4304 "replacement legacy gossip session is registered",
4305 );
4306
4307 let (stale_peer, _stale_peer_send) = legacy_gossip_peer_with_conn(
4308 peer_id.clone(),
4309 old_conn_id,
4310 tokio_util::sync::CancellationToken::new(),
4311 );
4312 sink.add_peer(stale_peer);
4313
4314 sink.remove_peer(&peer_id, old_conn_id);
4315 assert!(
4316 outbound.contains(&peer_id),
4317 "stale cleanup must not remove the replacement legacy gossip session",
4318 );
4319
4320 sink.remove_peer(&peer_id, new_conn_id);
4321 assert!(
4322 !outbound.contains(&peer_id),
4323 "live cleanup removes the replacement legacy gossip session",
4324 );
4325 }
4326
4327 #[tokio::test]
4328 async fn disconnected_peer_send_returns_error() -> Result<(), BoxError> {
4329 let peer_id = ZakuraPeerId::new(vec![9; 32]).expect("test peer id is within bounds");
4330 let (disconnected, rx) = framed_channel(1);
4331 drop(rx);
4332
4333 let error = send_to_sessions(
4334 vec![LegacyGossipPeerSession::new(peer_id, 0, disconnected)],
4335 LegacyGossipFrame::AdvertiseBlock(block_hash(8)),
4336 )
4337 .expect_err("closed outbound queue reports an adapter error");
4338 assert!(
4339 error
4340 .to_string()
4341 .contains("ordered stream send queue is closed"),
4342 "unexpected error: {error}"
4343 );
4344 Ok(())
4345 }
4346
4347 #[tokio::test]
4348 async fn transient_inbound_failure_does_not_poison_first_seen_cache() -> Result<(), BoxError> {
4349 let (request_tx, mut request_rx) = tokio::sync::mpsc::unbounded_channel();
4350 let attempts = Arc::new(AtomicUsize::new(0));
4351 let (inbound_tx, inbound_rx) = mpsc::channel(4);
4352 let supervisor = ZakuraSupervisorHandle::new(1);
4353 let peer_id = ZakuraPeerId::new(vec![3; 32]).expect("test peer id is within bounds");
4354 let frame = LegacyGossipFrame::AdvertiseBlock(block_hash(5));
4355
4356 let worker = tokio::spawn(legacy_gossip_worker(
4357 FailsOnceRecorder {
4358 attempts: attempts.clone(),
4359 tx: request_tx,
4360 },
4361 inbound_rx,
4362 LegacyGossipForwarder::new(supervisor),
4363 ZakuraTrace::noop(),
4364 ));
4365
4366 inbound_tx
4367 .send(LegacyInboundWork::Gossip(LegacyGossipInbound {
4368 peer_id: peer_id.clone(),
4369 frame: frame.clone(),
4370 }))
4371 .await?;
4372 inbound_tx
4373 .send(LegacyInboundWork::Gossip(LegacyGossipInbound {
4374 peer_id: peer_id.clone(),
4375 frame: frame.clone(),
4376 }))
4377 .await?;
4378
4379 match recv_request(&mut request_rx).await? {
4380 Request::AdvertiseBlock(hash, Some(PeerSource::Zakura(source))) => {
4381 assert_eq!(hash, block_hash(5));
4382 assert_eq!(source, peer_id);
4383 }
4384 request => panic!("unexpected request: {request:?}"),
4385 }
4386 assert_eq!(attempts.load(Ordering::SeqCst), 2);
4387
4388 inbound_tx
4389 .send(LegacyInboundWork::Gossip(LegacyGossipInbound {
4390 peer_id,
4391 frame,
4392 }))
4393 .await?;
4394 tokio::time::sleep(Duration::from_millis(100)).await;
4395 assert_eq!(
4396 attempts.load(Ordering::SeqCst),
4397 2,
4398 "successful delivery marks the inventory seen"
4399 );
4400
4401 drop(inbound_tx);
4402 worker.await?;
4403 Ok(())
4404 }
4405
4406 #[test]
4407 fn inbound_queue_full_drops_without_rejecting_peer_but_malformed_rejects() {
4408 let (inbound_tx, _inbound_rx) = mpsc::channel(1);
4409 let sink = LegacyGossipSink {
4410 inbound_tx,
4411 outbound: LegacyGossipOutbound::default(),
4412 trace: ZakuraTrace::noop(),
4413 };
4414 let peer_id = ZakuraPeerId::new(vec![4; 32]).expect("test peer id is within bounds");
4415 let frame = LegacyGossipFrame::AdvertiseBlock(block_hash(6))
4416 .encode_frame()
4417 .expect("frame encodes");
4418
4419 assert!(sink
4420 .deliver(peer_id.clone(), ZAKURA_STREAM_GOSSIP, frame.clone())
4421 .is_ok());
4422 assert!(
4423 sink.deliver(peer_id.clone(), ZAKURA_STREAM_GOSSIP, frame)
4424 .is_ok(),
4425 "queue-full overload is shed without disconnecting the peer"
4426 );
4427 assert!(sink
4428 .deliver(
4429 peer_id,
4430 ZAKURA_STREAM_GOSSIP,
4431 Frame {
4432 message_type: MSG_ADVERTISE_BLOCK,
4433 flags: 1,
4434 payload: Vec::new(),
4435 },
4436 )
4437 .is_err());
4438 }
4439
4440 #[test]
4441 fn tx_gossip_round_trips_legacy_and_witnessed_ids() {
4442 let frame =
4443 LegacyGossipFrame::AdvertiseTransactionIds(vec![legacy_tx_id(2), witnessed_tx_id(3)]);
4444
4445 assert_eq!(
4446 LegacyGossipFrame::decode_frame(frame.encode_frame().expect("frame encodes"))
4447 .expect("frame decodes"),
4448 frame
4449 );
4450 }
4451
4452 #[test]
4453 fn inventory_request_frames_round_trip_and_enforce_bounds() {
4454 let block_request = LegacyRequestFrame::BlocksByHash(vec![block_hash(1), block_hash(2)]);
4455 assert_eq!(
4456 LegacyRequestFrame::decode_frame(block_request.encode_frame().expect("frame encodes"))
4457 .expect("frame decodes"),
4458 block_request
4459 );
4460
4461 let tx_request =
4462 LegacyRequestFrame::TransactionsById(vec![legacy_tx_id(3), witnessed_tx_id(4)]);
4463 assert_eq!(
4464 LegacyRequestFrame::decode_frame(tx_request.encode_frame().expect("frame encodes"))
4465 .expect("frame decodes"),
4466 tx_request
4467 );
4468
4469 let find_blocks = LegacyRequestFrame::FindBlocks {
4470 known_blocks: vec![block_hash(5)],
4471 stop: Some(block_hash(6)),
4472 };
4473 assert_eq!(
4474 LegacyRequestFrame::decode_frame(find_blocks.encode_frame().expect("frame encodes"))
4475 .expect("frame decodes"),
4476 find_blocks
4477 );
4478
4479 let find_headers = LegacyRequestFrame::FindHeaders {
4480 known_blocks: vec![block_hash(7)],
4481 stop: None,
4482 };
4483 assert_eq!(
4484 LegacyRequestFrame::decode_frame(find_headers.encode_frame().expect("frame encodes"))
4485 .expect("frame decodes"),
4486 find_headers
4487 );
4488
4489 for request in [
4490 LegacyRequestFrame::MempoolTransactionIds,
4491 LegacyRequestFrame::Ping,
4492 LegacyRequestFrame::PushTransaction(UnminedTx::from(empty_v5_transaction(8))),
4493 ] {
4494 assert_eq!(
4495 LegacyRequestFrame::decode_frame(request.encode_frame().expect("frame encodes"))
4496 .expect("frame decodes"),
4497 request
4498 );
4499 }
4500
4501 let max = usize::try_from(MAX_TX_INV_IN_SENT_MESSAGE).expect("test cap fits usize");
4502 let oversized = Frame {
4503 message_type: MSG_REQUEST_BLOCKS_BY_HASH,
4504 flags: 0,
4505 payload: encoded_count(max + 1),
4506 };
4507 assert!(matches!(
4508 LegacyRequestFrame::decode_frame(oversized),
4509 Err(LegacyGossipError::TooManyInventoryItems(count)) if count == max + 1
4510 ));
4511
4512 let max_locator =
4513 usize::try_from(MAX_BLOCK_LOCATOR_LENGTH).expect("test locator cap fits usize");
4514 let oversized_locator = Frame {
4515 message_type: MSG_REQUEST_FIND_HEADERS,
4516 flags: 0,
4517 payload: encoded_count(max_locator + 1),
4518 };
4519 assert!(matches!(
4520 LegacyRequestFrame::decode_frame(oversized_locator),
4521 Err(LegacyGossipError::TooManyBlockLocatorHashes(count)) if count == max_locator + 1
4522 ));
4523 }
4524
4525 #[test]
4526 fn response_codec_rejects_wrong_request_id_and_oversized_chunks() {
4527 let mut wrong_id_payload = Vec::new();
4528 wrong_id_payload.extend_from_slice(&2_u64.to_le_bytes());
4529 wrong_id_payload.push(1);
4530 let wrong_id = Frame {
4531 message_type: MSG_RESPONSE_BLOCK,
4532 flags: 0,
4533 payload: wrong_id_payload,
4534 };
4535 assert!(matches!(
4536 LegacyResponseCodec::decode_response(
4537 1,
4538 LegacyRequestKind::Blocks,
4539 vec![wrong_id],
4540 None
4541 ),
4542 Err(LegacyGossipError::WrongRequestId {
4543 expected: 1,
4544 actual: 2
4545 })
4546 ));
4547
4548 let mut oversized_payload = Vec::new();
4549 oversized_payload.extend_from_slice(&1_u64.to_le_bytes());
4550 oversized_payload.push(0);
4551 oversized_payload.resize(
4552 MAX_PROTOCOL_MESSAGE_LEN + RESPONSE_CHUNK_HEADER_BYTES + 1,
4553 0,
4554 );
4555 let oversized = Frame {
4556 message_type: MSG_RESPONSE_BLOCK,
4557 flags: 0,
4558 payload: oversized_payload,
4559 };
4560 assert!(matches!(
4561 LegacyResponseCodec::decode_response(
4562 1,
4563 LegacyRequestKind::Blocks,
4564 vec![oversized],
4565 None
4566 ),
4567 Err(LegacyGossipError::OversizedResponse(_))
4568 ));
4569 }
4570
4571 #[test]
4575 fn nil_response_decodes_as_each_kinds_empty_result() {
4576 let request_id = 7;
4577 let nil = || id_only_frame(MSG_RESPONSE_NIL, request_id);
4578
4579 assert_eq!(
4580 LegacyResponseCodec::decode_response(
4581 request_id,
4582 LegacyRequestKind::FindBlocks,
4583 vec![nil()],
4584 None,
4585 )
4586 .expect("nil is a valid empty FindBlocks response"),
4587 Response::BlockHashes(vec![]),
4588 );
4589 assert_eq!(
4590 LegacyResponseCodec::decode_response(
4591 request_id,
4592 LegacyRequestKind::FindHeaders,
4593 vec![nil()],
4594 None,
4595 )
4596 .expect("nil is a valid empty FindHeaders response"),
4597 Response::BlockHeaders(vec![]),
4598 );
4599 assert_eq!(
4600 LegacyResponseCodec::decode_response(
4601 request_id,
4602 LegacyRequestKind::MempoolTransactionIds,
4603 vec![nil()],
4604 None,
4605 )
4606 .expect("nil is a valid empty mempool response"),
4607 Response::TransactionIds(vec![]),
4608 );
4609 assert_eq!(
4610 LegacyResponseCodec::decode_response(
4611 request_id,
4612 LegacyRequestKind::PushTransaction,
4613 vec![nil()],
4614 None,
4615 )
4616 .expect("nil acknowledges a pushed transaction"),
4617 Response::Nil,
4618 );
4619
4620 for kind in [
4624 LegacyRequestKind::Blocks,
4625 LegacyRequestKind::Transactions,
4626 LegacyRequestKind::Ping,
4627 ] {
4628 assert!(
4629 matches!(
4630 LegacyResponseCodec::decode_response(request_id, kind, vec![nil()], None),
4631 Err(LegacyGossipError::UnexpectedResponse(_)),
4632 ),
4633 "nil must be rejected for {kind:?} so the caller can fall back",
4634 );
4635 }
4636 }
4637
4638 #[test]
4639 fn response_codec_chunks_to_negotiated_frame_limit() -> Result<(), BoxError> {
4640 let block = Arc::new(Block::zcash_deserialize(
4641 BLOCK_TESTNET_141042_BYTES.as_slice(),
4642 )?);
4643 let max_frame_bytes =
4644 u32::try_from(FRAME_HEADER_BYTES + RESPONSE_CHUNK_HEADER_BYTES + 256)?;
4645 let frames = LegacyResponseCodec::encode_response(
4646 7,
4647 Response::Blocks(vec![InventoryResponse::Available((block.clone(), None))]),
4648 max_frame_bytes,
4649 max_frame_bytes,
4650 )?;
4651
4652 assert!(frames.len() > 1, "large block response must be chunked");
4653 for frame in &frames {
4654 frame.encode(max_frame_bytes)?;
4655 }
4656
4657 let response =
4658 LegacyResponseCodec::decode_response(7, LegacyRequestKind::Blocks, frames, None)?;
4659 let Response::Blocks(blocks) = response else {
4660 panic!("unexpected response: {response:?}");
4661 };
4662 assert!(matches!(
4663 blocks.as_slice(),
4664 [InventoryResponse::Available((received, None))] if received.hash() == block.hash()
4665 ));
4666
4667 Ok(())
4668 }
4669
4670 #[test]
4683 fn encode_response_chunks_respect_message_cap() -> Result<(), BoxError> {
4684 let block = Arc::new(Block::zcash_deserialize(
4685 BLOCK_TESTNET_141042_BYTES.as_slice(),
4686 )?);
4687
4688 let max_frame_bytes = u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?;
4692 let max_message_bytes = u32::try_from(RESPONSE_CHUNK_HEADER_BYTES + 256)?;
4693
4694 let frames = LegacyResponseCodec::encode_response(
4695 7,
4696 Response::Blocks(vec![InventoryResponse::Available((block.clone(), None))]),
4697 max_frame_bytes,
4698 max_message_bytes,
4699 )?;
4700
4701 assert!(
4702 frames.len() > 1,
4703 "a block larger than the negotiated message cap must be chunked, not emitted as one \
4704 over-cap frame"
4705 );
4706 for frame in &frames {
4707 assert!(
4708 frame.payload.len() <= max_message_bytes as usize,
4709 "chunk frame payload {} exceeds the negotiated max_message_bytes {}; the peer \
4710 (and write_response_frame) would reject it as oversize",
4711 frame.payload.len(),
4712 max_message_bytes,
4713 );
4714 frame.encode(max_frame_bytes)?;
4716 }
4717
4718 let response =
4720 LegacyResponseCodec::decode_response(7, LegacyRequestKind::Blocks, frames, None)?;
4721 let Response::Blocks(blocks) = response else {
4722 panic!("unexpected response: {response:?}");
4723 };
4724 assert!(matches!(
4725 blocks.as_slice(),
4726 [InventoryResponse::Available((received, None))] if received.hash() == block.hash()
4727 ));
4728
4729 Ok(())
4730 }
4731
4732 #[test]
4739 fn decode_response_binds_blocks_to_requested_hashes() -> Result<(), BoxError> {
4740 let block = Arc::new(Block::zcash_deserialize(
4741 BLOCK_TESTNET_141042_BYTES.as_slice(),
4742 )?);
4743 let frame_cap = u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?;
4744 let frames = LegacyResponseCodec::encode_response(
4745 7,
4746 Response::Blocks(vec![InventoryResponse::Available((block.clone(), None))]),
4747 frame_cap,
4748 frame_cap,
4749 )?;
4750
4751 let unrelated: HashSet<block::Hash> = std::iter::once(block_hash(99)).collect();
4755 assert!(
4756 matches!(
4757 LegacyResponseCodec::decode_response(
4758 7,
4759 LegacyRequestKind::Blocks,
4760 frames.clone(),
4761 Some(&unrelated),
4762 ),
4763 Err(LegacyGossipError::UnsolicitedBlock(_)),
4764 ),
4765 "a block whose hash was not requested must be rejected",
4766 );
4767
4768 let requested: HashSet<block::Hash> = std::iter::once(block.hash()).collect();
4770 let response = LegacyResponseCodec::decode_response(
4771 7,
4772 LegacyRequestKind::Blocks,
4773 frames,
4774 Some(&requested),
4775 )?;
4776 assert!(matches!(
4777 response,
4778 Response::Blocks(blocks)
4779 if matches!(
4780 blocks.as_slice(),
4781 [InventoryResponse::Available((received, None))]
4782 if received.hash() == block.hash()
4783 )
4784 ));
4785
4786 Ok(())
4787 }
4788
4789 #[test]
4800 fn encode_response_aborts_when_aggregation_exceeds_budget() -> Result<(), BoxError> {
4801 let block = Arc::new(Block::zcash_deserialize(
4802 BLOCK_TESTNET_141042_BYTES.as_slice(),
4803 )?);
4804 let block_bytes = block.zcash_serialize_to_vec()?.len();
4805 let frame_cap = u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?;
4806
4807 LegacyResponseCodec::encode_response(
4809 1,
4810 Response::Blocks(vec![InventoryResponse::Available((block.clone(), None))]),
4811 frame_cap,
4812 frame_cap,
4813 )?;
4814
4815 let block_copies = LEGACY_RESPONSE_MAX_AGGREGATE_BYTES / block_bytes + 2;
4818 let many_blocks = Response::Blocks(
4819 (0..block_copies)
4820 .map(|_| InventoryResponse::Available((block.clone(), None)))
4821 .collect(),
4822 );
4823 assert!(
4824 matches!(
4825 LegacyResponseCodec::encode_response(2, many_blocks, frame_cap, frame_cap),
4826 Err(LegacyGossipError::ResponseAggregateBudget(_)),
4827 ),
4828 "an over-budget BlocksByHash response must abort encoding early",
4829 );
4830
4831 let tx_copies = 2 * LEGACY_RESPONSE_MAX_AGGREGATE_BYTES / block_bytes + 2;
4834 let many_transactions = Response::Transactions(
4835 (0..tx_copies)
4836 .flat_map(|_| {
4837 block
4838 .transactions
4839 .iter()
4840 .map(|tx| InventoryResponse::Available((UnminedTx::from(tx.clone()), None)))
4841 })
4842 .collect(),
4843 );
4844 assert!(
4845 matches!(
4846 LegacyResponseCodec::encode_response(3, many_transactions, frame_cap, frame_cap),
4847 Err(LegacyGossipError::ResponseAggregateBudget(_)),
4848 ),
4849 "an over-budget TransactionsById response must abort encoding early",
4850 );
4851
4852 Ok(())
4853 }
4854
4855 #[test]
4856 fn response_codec_round_trips_chain_sync_and_mempool_responses() -> Result<(), BoxError> {
4857 let block = Arc::new(Block::zcash_deserialize(
4858 BLOCK_TESTNET_141042_BYTES.as_slice(),
4859 )?);
4860 let header = block::CountedHeader {
4861 header: block.header.clone(),
4862 };
4863
4864 let block_hash_response = Response::BlockHashes(vec![block.hash(), block_hash(10)]);
4865 let frames = LegacyResponseCodec::encode_response(
4866 8,
4867 block_hash_response.clone(),
4868 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4869 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4870 )?;
4871 assert_eq!(
4872 LegacyResponseCodec::decode_response(8, LegacyRequestKind::FindBlocks, frames, None)?,
4873 block_hash_response
4874 );
4875
4876 let header_response = Response::BlockHeaders(vec![header]);
4877 let frames = LegacyResponseCodec::encode_response(
4878 9,
4879 header_response.clone(),
4880 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4881 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4882 )?;
4883 assert_eq!(
4884 LegacyResponseCodec::decode_response(9, LegacyRequestKind::FindHeaders, frames, None)?,
4885 header_response
4886 );
4887
4888 let tx_ids_response = Response::TransactionIds(vec![legacy_tx_id(11), witnessed_tx_id(12)]);
4889 let frames = LegacyResponseCodec::encode_response(
4890 10,
4891 tx_ids_response.clone(),
4892 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4893 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4894 )?;
4895 assert_eq!(
4896 LegacyResponseCodec::decode_response(
4897 10,
4898 LegacyRequestKind::MempoolTransactionIds,
4899 frames,
4900 None,
4901 )?,
4902 tx_ids_response
4903 );
4904
4905 let frames = LegacyResponseCodec::encode_response(
4906 11,
4907 Response::Pong(Duration::ZERO),
4908 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4909 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4910 )?;
4911 assert!(matches!(
4912 LegacyResponseCodec::decode_response(11, LegacyRequestKind::Ping, frames, None)?,
4913 Response::Pong(_)
4914 ));
4915
4916 let frames = LegacyResponseCodec::encode_response(
4917 12,
4918 Response::Nil,
4919 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4920 u32::try_from(MAX_PROTOCOL_MESSAGE_LEN)?,
4921 )?;
4922 assert_eq!(
4923 LegacyResponseCodec::decode_response(
4924 12,
4925 LegacyRequestKind::PushTransaction,
4926 frames,
4927 None
4928 )?,
4929 Response::Nil
4930 );
4931
4932 Ok(())
4933 }
4934
4935 #[test]
4936 fn malformed_frames_are_rejected() {
4937 assert!(matches!(
4938 LegacyGossipFrame::decode_frame(Frame {
4939 message_type: MSG_ADVERTISE_BLOCK,
4940 flags: 1,
4941 payload: Vec::new(),
4942 }),
4943 Err(LegacyGossipError::UnsupportedFlags(1))
4944 ));
4945 assert!(matches!(
4946 LegacyGossipFrame::decode_frame(Frame {
4947 message_type: 99,
4948 flags: 0,
4949 payload: Vec::new(),
4950 }),
4951 Err(LegacyGossipError::UnknownMessageType(99))
4952 ));
4953 assert!(matches!(
4954 LegacyGossipFrame::decode_frame(Frame {
4955 message_type: MSG_ADVERTISE_TX_IDS,
4956 flags: 0,
4957 payload: vec![0],
4958 }),
4959 Err(LegacyGossipError::EmptyTransactionAdvertisement)
4960 ));
4961 }
4962
4963 #[test]
4964 fn tx_id_limits_are_enforced_before_allocation() {
4965 let max = usize::try_from(MAX_TX_INV_IN_SENT_MESSAGE).expect("test cap fits usize");
4966 let oversized = Frame {
4967 message_type: MSG_ADVERTISE_TX_IDS,
4968 flags: 0,
4969 payload: encoded_count(max + 1),
4970 };
4971 assert!(matches!(
4972 LegacyGossipFrame::decode_frame(oversized),
4973 Err(LegacyGossipError::TooManyInventoryItems(count)) if count == max + 1
4974 ));
4975
4976 let ids: HashSet<_> = (0..max + 3)
4977 .map(|index| {
4978 let mut bytes = [0; 32];
4979 bytes[..8].copy_from_slice(
4980 &u64::try_from(index)
4981 .expect("test index fits u64")
4982 .to_le_bytes(),
4983 );
4984 UnminedTxId::from_legacy_id(transaction::Hash(bytes))
4985 })
4986 .collect();
4987 let frame = LegacyGossipFrame::from_request(Request::AdvertiseTransactionIds(ids, None))
4988 .expect("non-empty tx gossip converts");
4989 let LegacyGossipFrame::AdvertiseTransactionIds(ids) = frame else {
4990 panic!("expected tx id frame");
4991 };
4992 assert!(ids.len() <= max);
4993 }
4994
4995 #[test]
4996 fn unsupported_requests_fail_loudly() {
4997 let unsupported = [
4998 (
4999 Request::BlocksByHash(HashSet::from([block_hash(1)])),
5000 "BlocksByHash",
5001 ),
5002 (
5003 Request::TransactionsById(HashSet::from([legacy_tx_id(1)])),
5004 "TransactionsById",
5005 ),
5006 (
5007 Request::FindBlocks {
5008 known_blocks: vec![block_hash(2)],
5009 stop: None,
5010 },
5011 "FindBlocks",
5012 ),
5013 (
5014 Request::FindHeaders {
5015 known_blocks: vec![block_hash(3)],
5016 stop: None,
5017 },
5018 "FindHeaders",
5019 ),
5020 (
5021 Request::PushTransaction(
5022 Transaction::V5 {
5023 network_upgrade: NetworkUpgrade::Nu5,
5024 lock_time: LockTime::min_lock_time_timestamp(),
5025 expiry_height: block::Height(0),
5026 inputs: Vec::new(),
5027 outputs: Vec::new(),
5028 sapling_shielded_data: None,
5029 orchard_shielded_data: None,
5030 }
5031 .into(),
5032 None,
5033 ),
5034 "PushTransaction",
5035 ),
5036 (Request::MempoolTransactionIds, "MempoolTransactionIds"),
5037 (Request::Peers, "Peers"),
5038 ];
5039
5040 for (request, command) in unsupported {
5041 let error =
5042 LegacyGossipFrame::from_request(request).expect_err("request is unsupported");
5043 assert!(matches!(
5044 error,
5045 LegacyGossipError::UnsupportedRequest(unsupported) if unsupported == command
5046 ));
5047 }
5048 }
5049
5050 #[test]
5051 fn legacy_peers_request_is_deferred_to_native_bootstrap_configuration() {
5052 let error =
5053 LegacyRequestFrame::from_request(Request::Peers).expect_err("Peers is deferred");
5054 assert!(matches!(
5055 error,
5056 LegacyGossipError::UnsupportedRequest("Peers")
5057 ));
5058 assert!(
5059 error
5060 .to_string()
5061 .contains("unsupported legacy gossip request"),
5062 "unexpected error: {error}"
5063 );
5064 }
5065
5066 #[tokio::test]
5067 async fn first_seen_cache_is_bounded_and_expires() {
5068 let cache = FirstSeenCache::new(1, Duration::from_millis(20));
5069 assert_eq!(
5070 cache
5071 .record_unseen([InventoryKey::Block(block_hash(1))])
5072 .await,
5073 vec![InventoryKey::Block(block_hash(1))]
5074 );
5075 assert!(cache
5076 .record_unseen([InventoryKey::Block(block_hash(1))])
5077 .await
5078 .is_empty());
5079 assert_eq!(
5080 cache
5081 .record_unseen([InventoryKey::Block(block_hash(2))])
5082 .await,
5083 vec![InventoryKey::Block(block_hash(2))]
5084 );
5085 assert_eq!(
5086 cache
5087 .record_unseen([InventoryKey::Block(block_hash(1))])
5088 .await,
5089 vec![InventoryKey::Block(block_hash(1))]
5090 );
5091
5092 let cache = FirstSeenCache::new(8, Duration::from_millis(1));
5093 assert_eq!(
5094 cache
5095 .record_unseen([InventoryKey::Block(block_hash(3))])
5096 .await,
5097 vec![InventoryKey::Block(block_hash(3))]
5098 );
5099 tokio::time::sleep(Duration::from_millis(5)).await;
5100 assert_eq!(
5101 cache
5102 .record_unseen([InventoryKey::Block(block_hash(3))])
5103 .await,
5104 vec![InventoryKey::Block(block_hash(3))]
5105 );
5106 }
5107
5108 #[tokio::test]
5109 async fn malformed_inbound_gossip_disconnects_peer() -> Result<(), BoxError> {
5110 let _guard = zakura_test::init();
5111 let (node, _rx) = legacy_node(31).await?;
5112 let hostile =
5113 HostilePeer::connect_native_with_capabilities(&node, 32, ZAKURA_CAP_LEGACY_GOSSIP)
5114 .await?;
5115 wait_registered_count(&node, 1).await?;
5116
5117 hostile.send_frame(ZAKURA_STREAM_GOSSIP, vec![1]).await?;
5118 wait_registered_count(&node, 0).await?;
5119
5120 hostile.shutdown().await;
5121 node.shutdown().await;
5122 Ok(())
5123 }
5124
5125 #[tokio::test]
5126 async fn gossip_stream_with_request_id_disconnects_peer() -> Result<(), BoxError> {
5127 let _guard = zakura_test::init();
5128 let node = ZakuraTestNode::builder(41).spawn().await?;
5129 let hostile = HostilePeer::connect_native(&node, 42).await?;
5130 wait_registered_count(&node, 1).await?;
5131
5132 let frame = LegacyGossipFrame::AdvertiseBlock(block_hash(77)).encode_frame()?;
5133 hostile
5134 .send_frame_with_request_id(ZAKURA_STREAM_GOSSIP, 99, frame)
5135 .await?;
5136 wait_registered_count(&node, 0).await?;
5137
5138 hostile.shutdown().await;
5139 node.shutdown().await;
5140 Ok(())
5141 }
5142
5143 #[tokio::test]
5144 async fn request_stream_without_request_id_disconnects_peer() -> Result<(), BoxError> {
5145 let _guard = zakura_test::init();
5146 let node = ZakuraTestNode::builder(71).spawn().await?;
5147 let hostile = HostilePeer::connect_native(&node, 72).await?;
5148 wait_registered_count(&node, 1).await?;
5149
5150 let frame = LegacyRequestFrame::BlocksByHash(vec![block_hash(1)]).encode_frame()?;
5151 hostile
5152 .send_frame(ZAKURA_STREAM_LEGACY_REQUESTS, frame.payload)
5153 .await?;
5154 wait_registered_count(&node, 0).await?;
5155
5156 hostile.shutdown().await;
5157 node.shutdown().await;
5158 Ok(())
5159 }
5160
5161 #[tokio::test]
5162 async fn malformed_outbound_response_disconnects_peer() -> Result<(), BoxError> {
5163 let _guard = zakura_test::init();
5164 let node = ZakuraTestNode::builder(73).spawn().await?;
5165 let hostile = HostilePeer::connect_native(&node, 74).await?;
5166 wait_registered_count(&node, 1).await?;
5167 let hostile_id = hostile.id()?;
5168
5169 let mut wrong_id_payload = Vec::new();
5170 wrong_id_payload.extend_from_slice(&0_u64.to_le_bytes());
5171 wrong_id_payload.push(1);
5172 let response = Frame {
5173 message_type: MSG_RESPONSE_BLOCK,
5174 flags: 0,
5175 payload: wrong_id_payload,
5176 };
5177
5178 let adapter = LegacyRequestAdapter::new(node.supervisor());
5179 let request = adapter.request_from_source(
5180 Request::BlocksByHash(HashSet::from([block_hash(1)])),
5181 Some(PeerSource::Zakura(hostile_id)),
5182 );
5183 let responder = hostile.respond_to_next_request(vec![response]);
5184 let (request_result, responder_result) = futures::join!(request, responder);
5185
5186 responder_result?;
5187 let error = request_result.expect_err("wrong request id is a request error");
5188 assert!(
5189 error
5190 .to_string()
5191 .contains("wrong legacy response request id"),
5192 "unexpected error: {error}"
5193 );
5194 wait_registered_count(&node, 0).await?;
5195
5196 hostile.shutdown().await;
5197 node.shutdown().await;
5198 Ok(())
5199 }
5200
5201 #[tokio::test]
5202 async fn outbound_request_timeout_releases_peer_for_next_request() -> Result<(), BoxError> {
5203 let _guard = zakura_test::init();
5204 let node = ZakuraTestNode::builder(79).spawn().await?;
5205 let mut hostile = HostilePeer::connect_native(&node, 80).await?;
5206 wait_registered_count(&node, 1).await?;
5207 let hostile_id = hostile.id()?;
5208
5209 let adapter =
5210 LegacyRequestAdapter::new_with_timeout(node.supervisor(), Duration::from_millis(100));
5211 let first_request = adapter.request_from_source(
5212 Request::BlocksByHash(HashSet::from([block_hash(1)])),
5213 Some(PeerSource::Zakura(hostile_id.clone())),
5214 );
5215 let hold_first = hostile.accept_next_request_without_response();
5216 let (request_result, hold_result) = futures::join!(first_request, hold_first);
5217
5218 hold_result?;
5219 let error = request_result.expect_err("silent peer should time out the request");
5220 assert!(
5221 error.to_string().contains("timed out"),
5222 "unexpected error: {error}"
5223 );
5224 wait_registered_count(&node, 1).await?;
5225
5226 let second_hash = block_hash(2);
5227 let second_request = adapter.request_from_source(
5228 Request::BlocksByHash(HashSet::from([second_hash])),
5229 Some(PeerSource::Zakura(hostile_id)),
5230 );
5231 let second_response = hostile.respond_to_next_request_with(|request_id| {
5232 vec![missing_blocks_frame(request_id, vec![second_hash])
5233 .expect("one missing block hash fits in a response frame")]
5234 });
5235 let (request_result, response_result) = futures::join!(second_request, second_response);
5236
5237 response_result?;
5238 assert_eq!(
5239 request_result?,
5240 Response::Blocks(vec![InventoryResponse::Missing(second_hash)])
5241 );
5242
5243 hostile.shutdown().await;
5244 node.shutdown().await;
5245 Ok(())
5246 }
5247
5248 #[tokio::test]
5249 async fn excessive_outbound_response_frames_disconnects_peer() -> Result<(), BoxError> {
5250 let _guard = zakura_test::init();
5251 let node = ZakuraTestNode::builder(75).spawn().await?;
5252 let hostile = HostilePeer::connect_native(&node, 76).await?;
5253 wait_registered_count(&node, 1).await?;
5254 let hostile_id = hostile.id()?;
5255
5256 let adapter = LegacyRequestAdapter::new(node.supervisor());
5257 let request = adapter.request_from_source(
5258 Request::BlocksByHash(HashSet::from([block_hash(1)])),
5259 Some(PeerSource::Zakura(hostile_id)),
5260 );
5261 let responder = hostile.respond_to_next_request_with(|request_id| {
5262 let mut frames = Vec::new();
5263 for _ in 0..10 {
5264 let mut payload = Vec::new();
5265 payload.extend_from_slice(&request_id.to_le_bytes());
5266 payload.extend_from_slice(&encoded_count(0));
5267 frames.push(Frame {
5268 message_type: MSG_RESPONSE_MISSING_BLOCKS,
5269 flags: 0,
5270 payload,
5271 });
5272 }
5273 frames
5274 });
5275 let (request_result, responder_result) = futures::join!(request, responder);
5276
5277 responder_result?;
5278 let error = request_result.expect_err("excessive response frames are rejected");
5279 assert!(
5280 error
5281 .to_string()
5282 .contains("too many legacy response frames"),
5283 "unexpected error: {error}"
5284 );
5285 wait_registered_count(&node, 0).await?;
5286
5287 hostile.shutdown().await;
5288 node.shutdown().await;
5289 Ok(())
5290 }
5291
5292 #[tokio::test]
5293 async fn invalid_outbound_block_response_disconnects_peer() -> Result<(), BoxError> {
5294 let _guard = zakura_test::init();
5295 let node = ZakuraTestNode::builder(77).spawn().await?;
5296 let hostile = HostilePeer::connect_native(&node, 78).await?;
5297 wait_registered_count(&node, 1).await?;
5298 let hostile_id = hostile.id()?;
5299
5300 let adapter = LegacyRequestAdapter::new(node.supervisor());
5301 let request = adapter.request_from_source(
5302 Request::BlocksByHash(HashSet::from([block_hash(1)])),
5303 Some(PeerSource::Zakura(hostile_id)),
5304 );
5305 let responder = hostile.respond_to_next_request_with(|request_id| {
5306 let mut payload = Vec::new();
5307 payload.extend_from_slice(&request_id.to_le_bytes());
5308 payload.push(1);
5309 payload.extend_from_slice(b"not a serialized block");
5310 vec![Frame {
5311 message_type: MSG_RESPONSE_BLOCK,
5312 flags: 0,
5313 payload,
5314 }]
5315 });
5316 let (request_result, responder_result) = futures::join!(request, responder);
5317
5318 responder_result?;
5319 request_result.expect_err("invalid block bytes are rejected");
5320 wait_registered_count(&node, 0).await?;
5321
5322 hostile.shutdown().await;
5323 node.shutdown().await;
5324 Ok(())
5325 }
5326
5327 #[derive(Clone)]
5329 enum StubInventory {
5330 Missing,
5332 Available(UnminedTx),
5334 Error,
5336 }
5337
5338 #[derive(Clone)]
5341 struct DualStackLegacyStub {
5342 tx: tokio::sync::mpsc::UnboundedSender<Request>,
5343 inventory: StubInventory,
5344 }
5345
5346 impl Service<Request> for DualStackLegacyStub {
5347 type Response = Response;
5348 type Error = BoxError;
5349 type Future = std::future::Ready<Result<Response, BoxError>>;
5350
5351 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
5352 Poll::Ready(Ok(()))
5353 }
5354
5355 fn call(&mut self, request: Request) -> Self::Future {
5356 let _ = self.tx.send(request.clone());
5357 let response = match request {
5358 Request::TransactionsById(ids) | Request::TransactionsByIdFrom { ids, .. } => {
5359 match &self.inventory {
5360 StubInventory::Error => {
5361 return std::future::ready(Err("legacy inventory unavailable".into()))
5362 }
5363 StubInventory::Missing => Response::Transactions(
5364 ids.into_iter().map(InventoryResponse::Missing).collect(),
5365 ),
5366 StubInventory::Available(tx) => Response::Transactions(
5367 ids.into_iter()
5368 .map(|id| {
5369 if id == tx.id {
5370 InventoryResponse::Available((tx.clone(), None))
5371 } else {
5372 InventoryResponse::Missing(id)
5373 }
5374 })
5375 .collect(),
5376 ),
5377 }
5378 }
5379 Request::BlocksByHash(hashes) | Request::BlocksByHashFrom { hashes, .. } => {
5380 if let StubInventory::Error = self.inventory {
5381 return std::future::ready(Err("legacy inventory unavailable".into()));
5382 }
5383 Response::Blocks(hashes.into_iter().map(InventoryResponse::Missing).collect())
5384 }
5385 _ => Response::Nil,
5386 };
5387 std::future::ready(Ok(response))
5388 }
5389 }
5390
5391 #[tokio::test]
5392 async fn dual_stack_advertise_fans_out_to_legacy_and_zakura() -> Result<(), BoxError> {
5393 let _guard = zakura_test::init();
5394 let (node_a, _rx_a) = legacy_node(201).await?;
5396 let (node_b, mut rx_b) = legacy_node(202).await?;
5397 node_a.connect_native(&node_b, TEST_NET_TIMEOUT).await?;
5398 let a_peer_id = node_peer_id(&node_a).await?;
5399
5400 let (legacy_tx, mut rx_legacy) = tokio::sync::mpsc::unbounded_channel();
5401 let mut composite = ZakuraDualStackService::new(
5402 DualStackLegacyStub {
5403 tx: legacy_tx,
5404 inventory: StubInventory::Missing,
5405 },
5406 node_a.supervisor(),
5407 true,
5408 );
5409
5410 let hash = block_hash(9);
5411 composite
5412 .ready()
5413 .await?
5414 .call(Request::AdvertiseBlockToAll(hash))
5415 .await?;
5416
5417 match recv_request(&mut rx_legacy).await? {
5419 Request::AdvertiseBlockToAll(received) => assert_eq!(received, hash),
5420 request => panic!("unexpected legacy request: {request:?}"),
5421 }
5422 match recv_request(&mut rx_b).await? {
5424 Request::AdvertiseBlock(received, Some(PeerSource::Zakura(peer_id))) => {
5425 assert_eq!(received, hash);
5426 assert_eq!(peer_id, a_peer_id);
5427 }
5428 request => panic!("unexpected zakura request: {request:?}"),
5429 }
5430
5431 node_a.shutdown().await;
5432 node_b.shutdown().await;
5433 Ok(())
5434 }
5435
5436 #[tokio::test]
5437 async fn dual_stack_inventory_falls_back_to_zakura_when_legacy_missing() -> Result<(), BoxError>
5438 {
5439 let _guard = zakura_test::init();
5440 let transaction = UnminedTx::from(empty_v5_transaction(7));
5441 let advertiser = inventory_node(203, transaction.clone()).await?;
5442 let requester = ZakuraTestNode::builder(204).spawn().await?;
5443 requester
5444 .connect_native(&advertiser, TEST_NET_TIMEOUT)
5445 .await?;
5446
5447 let (legacy_tx, _rx_legacy) = tokio::sync::mpsc::unbounded_channel();
5448 let mut composite = ZakuraDualStackService::new(
5449 DualStackLegacyStub {
5450 tx: legacy_tx,
5451 inventory: StubInventory::Missing,
5452 },
5453 requester.supervisor(),
5454 true,
5455 );
5456
5457 let response = composite
5458 .ready()
5459 .await?
5460 .call(Request::TransactionsById(HashSet::from([transaction.id])))
5461 .await?;
5462 match response {
5463 Response::Transactions(items) => {
5464 assert_eq!(items.len(), 1);
5465 assert!(matches!(items[0], InventoryResponse::Available(_)));
5466 }
5467 other => panic!("unexpected response: {other:?}"),
5468 }
5469
5470 advertiser.shutdown().await;
5471 requester.shutdown().await;
5472 Ok(())
5473 }
5474
5475 #[tokio::test]
5476 async fn dual_stack_inventory_uses_legacy_when_available() -> Result<(), BoxError> {
5477 let _guard = zakura_test::init();
5478 let transaction = UnminedTx::from(empty_v5_transaction(8));
5479 let requester = ZakuraTestNode::builder(205).spawn().await?;
5482
5483 let (legacy_tx, _rx_legacy) = tokio::sync::mpsc::unbounded_channel();
5484 let mut composite = ZakuraDualStackService::new(
5485 DualStackLegacyStub {
5486 tx: legacy_tx,
5487 inventory: StubInventory::Available(transaction.clone()),
5488 },
5489 requester.supervisor(),
5490 true,
5491 );
5492
5493 let response = composite
5494 .ready()
5495 .await?
5496 .call(Request::TransactionsById(HashSet::from([transaction.id])))
5497 .await?;
5498 match response {
5499 Response::Transactions(items) => {
5500 assert!(matches!(items[0], InventoryResponse::Available(_)));
5501 }
5502 other => panic!("unexpected response: {other:?}"),
5503 }
5504
5505 requester.shutdown().await;
5506 Ok(())
5507 }
5508
5509 #[tokio::test]
5510 async fn dual_stack_zakura_only_bypasses_legacy_for_inventory() -> Result<(), BoxError> {
5511 let _guard = zakura_test::init();
5512 let transaction = UnminedTx::from(empty_v5_transaction(9));
5513 let advertiser = inventory_node(206, transaction.clone()).await?;
5514 let requester = ZakuraTestNode::builder(207).spawn().await?;
5515 requester
5516 .connect_native(&advertiser, TEST_NET_TIMEOUT)
5517 .await?;
5518
5519 let (legacy_tx, mut rx_legacy) = tokio::sync::mpsc::unbounded_channel();
5521 let mut composite = ZakuraDualStackService::new(
5522 DualStackLegacyStub {
5523 tx: legacy_tx,
5524 inventory: StubInventory::Error,
5525 },
5526 requester.supervisor(),
5527 false,
5528 );
5529
5530 let response = composite
5531 .ready()
5532 .await?
5533 .call(Request::TransactionsById(HashSet::from([transaction.id])))
5534 .await?;
5535 match response {
5536 Response::Transactions(items) => {
5537 assert!(matches!(items[0], InventoryResponse::Available(_)));
5538 }
5539 other => panic!("unexpected response: {other:?}"),
5540 }
5541 assert!(rx_legacy.try_recv().is_err());
5543
5544 advertiser.shutdown().await;
5545 requester.shutdown().await;
5546 Ok(())
5547 }
5548
5549 #[tokio::test]
5550 async fn dual_stack_zakura_only_routes_chain_sync_and_mempool_requests() -> Result<(), BoxError>
5551 {
5552 let _guard = zakura_test::init();
5553 let block = Arc::new(Block::zcash_deserialize(
5554 BLOCK_TESTNET_141042_BYTES.as_slice(),
5555 )?);
5556 let transaction = UnminedTx::from(empty_v5_transaction(33));
5557 let (responder, mut pushed_rx) =
5558 normal_network_node(220, block.clone(), transaction.clone()).await?;
5559 let requester = ZakuraTestNode::builder(221).spawn().await?;
5560 requester
5561 .connect_native(&responder, TEST_NET_TIMEOUT)
5562 .await?;
5563
5564 let (legacy_tx, mut rx_legacy) = tokio::sync::mpsc::unbounded_channel();
5569 let mut composite = ZakuraDualStackService::new(
5570 DualStackLegacyStub {
5571 tx: legacy_tx,
5572 inventory: StubInventory::Error,
5573 },
5574 requester.supervisor(),
5575 false,
5576 );
5577
5578 let find_blocks = composite
5579 .ready()
5580 .await?
5581 .call(Request::FindBlocks {
5582 known_blocks: vec![block_hash(1)],
5583 stop: Some(block.hash()),
5584 })
5585 .await?;
5586 match find_blocks {
5587 Response::BlockHashes(hashes) => assert_eq!(hashes, vec![block.hash()]),
5588 other => panic!("unexpected FindBlocks response: {other:?}"),
5589 }
5590
5591 let find_headers = composite
5592 .ready()
5593 .await?
5594 .call(Request::FindHeaders {
5595 known_blocks: vec![block_hash(1)],
5596 stop: Some(block.hash()),
5597 })
5598 .await?;
5599 assert!(
5600 matches!(find_headers, Response::BlockHeaders(ref headers) if headers.len() == 1),
5601 "FindHeaders should be served over Zakura, got {find_headers:?}",
5602 );
5603
5604 let mempool_ids = composite
5605 .ready()
5606 .await?
5607 .call(Request::MempoolTransactionIds)
5608 .await?;
5609 assert!(
5610 matches!(mempool_ids, Response::TransactionIds(ref ids) if *ids == vec![transaction.id]),
5611 "MempoolTransactionIds should be served over Zakura, got {mempool_ids:?}",
5612 );
5613
5614 composite
5615 .ready()
5616 .await?
5617 .call(Request::PushTransaction(transaction.clone(), None))
5618 .await?;
5619 let pushed = pushed_rx
5620 .recv()
5621 .await
5622 .expect("transaction pushed over Zakura");
5623 assert_eq!(pushed, transaction.id);
5624
5625 assert!(
5627 rx_legacy.try_recv().is_err(),
5628 "Zakura-only mode must not consult the legacy peer set for serviceable requests",
5629 );
5630
5631 responder.shutdown().await;
5632 requester.shutdown().await;
5633 Ok(())
5634 }
5635
5636 #[tokio::test]
5637 async fn dual_stack_passthrough_request_reaches_legacy() -> Result<(), BoxError> {
5638 let _guard = zakura_test::init();
5639 let node = ZakuraTestNode::builder(208).spawn().await?;
5640 let (legacy_tx, mut rx_legacy) = tokio::sync::mpsc::unbounded_channel();
5641 let mut composite = ZakuraDualStackService::new(
5642 DualStackLegacyStub {
5643 tx: legacy_tx,
5644 inventory: StubInventory::Missing,
5645 },
5646 node.supervisor(),
5647 true,
5648 );
5649
5650 composite.ready().await?.call(Request::Peers).await?;
5651 match recv_request(&mut rx_legacy).await? {
5652 Request::Peers => {}
5653 request => panic!("unexpected passthrough request: {request:?}"),
5654 }
5655
5656 node.shutdown().await;
5657 Ok(())
5658 }
5659}