1use std::{
4 cmp::Reverse,
5 collections::{HashMap, HashSet},
6 fmt,
7 io::{self, Cursor, Read, Write},
8 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
9 sync::Arc,
10 time::{Duration, SystemTime, UNIX_EPOCH},
11};
12
13use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
14use iroh::{NodeAddr, NodeId, SecretKey};
15use rand::seq::IteratorRandom;
16use thiserror::Error;
17use tokio::sync::{watch, Mutex};
18use zakura_chain::{
19 block,
20 primitives::ed25519::{Signature, SigningKey, VerificationKey},
21};
22
23use crate::zakura::{
24 BlockSyncStatus, ServiceAdmissionDecision, ServicePeerDirection, ServicePeerLimits,
25 ServicePeerSnapshot, ZakuraConnId, ZakuraNetworkId, ZakuraPeerId, MAX_BS_BLOCKS_PER_REQUEST,
26 MAX_BS_RESPONSE_BYTES,
27};
28
29pub const ZAKURA_STREAM_DISCOVERY: u16 = 4;
31pub const ZAKURA_DISCOVERY_STREAM_VERSION: u16 = 1;
33
34pub const MSG_DISCOVERY_HELLO: u8 = 1;
36pub const MSG_DISCOVERY_GET_PEERS: u8 = 2;
38pub const MSG_DISCOVERY_PEERS: u8 = 3;
40pub const MSG_DISCOVERY_GET_SERVICES: u8 = 4;
42pub const MSG_DISCOVERY_SERVICES: u8 = 5;
44
45pub const MAX_DISCOVERY_MESSAGE_BYTES: usize = 16 * 1024;
47pub const MAX_NODE_RECORD_BODY_BYTES: usize = 16 * 1024;
49pub const MAX_DIRECT_ADDRS_PER_RECORD: usize = 8;
51pub const MAX_SERVICES_PER_RECORD: usize = 32;
53pub const MAX_GET_SERVICES_WANTED: usize = 32;
55pub const MAX_SERVICE_SUMMARIES_PER_RESPONSE: usize = 32;
57pub const MAX_SERVICE_SUMMARY_BYTES: usize = 1024;
59pub const MAX_DISCOVERY_RECORDS_PER_RESPONSE: usize = 32;
61pub const MAX_DISCOVERY_EXCLUDED_NODE_IDS: usize = 256;
63pub const MAX_ZAKURA_SERVICE_ID_BYTES: usize = 64;
65pub const DEFAULT_DISCOVERY_RECORD_TTL: Duration = Duration::from_secs(24 * 60 * 60);
67pub const DEFAULT_DISCOVERY_REFRESH_INTERVAL: Duration = Duration::from_secs(10 * 60);
69pub const DEFAULT_DISCOVERY_PEER_SAMPLE_LIMIT: usize = 32;
71pub const DEFAULT_DISCOVERY_MAX_RECORD_TTL: Duration = Duration::from_secs(24 * 60 * 60);
73pub const DEFAULT_DISCOVERY_CLOCK_SKEW_TOLERANCE: Duration = Duration::from_secs(5 * 60);
75pub const DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE: Duration = Duration::from_secs(60);
77pub const DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX: Duration = Duration::from_secs(60 * 60);
79pub const DEFAULT_MAX_CONCURRENT_DISCOVERY_DIALS: usize = 4;
81pub const DEFAULT_DISCOVERY_CONNECTION_HEADROOM: usize = 4;
83pub const DEFAULT_DISCOVERY_ZAKURA_MAX_CONNECTIONS: usize = 32;
85pub const DEFAULT_LIVE_SERVICE_SUMMARY_TTL: Duration = Duration::from_secs(30);
87
88pub const SERVICE_ID_DISCOVERY: &str = "zakura.discovery.v1";
90pub const SERVICE_ID_HEADER_SYNC: &str = "zakura.header_sync.v2";
95pub const SERVICE_ID_HEADER_SYNC_RETIRED: &str = "zakura.header_sync.v1";
98pub const SERVICE_ID_BLOCK_SYNC: &str = "zakura.block_sync.v1";
100pub const SERVICE_ID_LEGACY_GOSSIP: &str = "zakura.legacy_gossip.v1";
102pub const SERVICE_ID_LEGACY_REQUESTS: &str = "zakura.legacy_requests.v1";
104pub const SERVICE_ID_SERVICE_DISCOVERY: &str = "zakura.service_discovery.v1";
106pub const SUMMARY_TAG_HEADER_SYNC_RETIRED: u16 = 1;
114pub const SUMMARY_TAG_DISCOVERY_V1: u16 = 2;
116pub const SUMMARY_TAG_BLOCK_SYNC_V1: u16 = 3;
118pub const SUMMARY_TAG_HEADER_SYNC: u16 = 4;
124
125const SIGNATURE_BYTES: usize = 64;
126const NODE_ID_BYTES: usize = 32;
127const SOCKET_ADDR_V4: u8 = 4;
128const SOCKET_ADDR_V6: u8 = 6;
129const ZAKURA_NODE_RECORD_SIG_DOMAIN: &[u8] = b"zakura-node-record-v1";
130const ZAKURA_NODE_RECORD_FORMAT_VERSION: u16 = ZAKURA_DISCOVERY_STREAM_VERSION;
131
132#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub struct ZakuraServiceId(String);
135
136impl ZakuraServiceId {
137 pub fn new(value: impl Into<String>) -> Result<Self, DiscoveryWireError> {
139 let value = value.into();
140 validate_service_id(value.as_bytes())?;
141 Ok(Self(value))
142 }
143
144 pub fn as_str(&self) -> &str {
146 &self.0
147 }
148
149 pub fn discovery() -> Self {
151 Self::new(SERVICE_ID_DISCOVERY)
152 .expect("built-in Zakura discovery service id is non-empty bounded ASCII")
153 }
154
155 pub fn header_sync() -> Self {
157 Self::new(SERVICE_ID_HEADER_SYNC)
158 .expect("built-in Zakura header-sync service id is non-empty bounded ASCII")
159 }
160
161 pub fn header_sync_retired() -> Self {
165 Self::new(SERVICE_ID_HEADER_SYNC_RETIRED)
166 .expect("built-in retired Zakura header-sync service id is non-empty bounded ASCII")
167 }
168
169 pub fn block_sync() -> Self {
171 Self::new(SERVICE_ID_BLOCK_SYNC)
172 .expect("built-in Zakura block-sync service id is non-empty bounded ASCII")
173 }
174
175 pub fn legacy_gossip() -> Self {
177 Self::new(SERVICE_ID_LEGACY_GOSSIP)
178 .expect("built-in Zakura legacy gossip service id is non-empty bounded ASCII")
179 }
180
181 pub fn legacy_requests() -> Self {
183 Self::new(SERVICE_ID_LEGACY_REQUESTS)
184 .expect("built-in Zakura legacy requests service id is non-empty bounded ASCII")
185 }
186
187 pub fn service_discovery() -> Self {
189 Self::new(SERVICE_ID_SERVICE_DISCOVERY)
190 .expect("built-in Zakura service discovery id is non-empty bounded ASCII")
191 }
192}
193
194impl fmt::Display for ZakuraServiceId {
195 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196 formatter.write_str(self.as_str())
197 }
198}
199
200impl TryFrom<&str> for ZakuraServiceId {
201 type Error = DiscoveryWireError;
202
203 fn try_from(value: &str) -> Result<Self, Self::Error> {
204 Self::new(value)
205 }
206}
207
208impl TryFrom<String> for ZakuraServiceId {
209 type Error = DiscoveryWireError;
210
211 fn try_from(value: String) -> Result<Self, Self::Error> {
212 Self::new(value)
213 }
214}
215
216#[derive(Clone, Debug, Eq, PartialEq)]
218pub struct ZakuraNodeRecordBody {
219 pub node_id: NodeId,
221 pub direct_addrs: Vec<SocketAddr>,
223 pub services: Vec<ZakuraServiceId>,
225 pub zakura_protocol_min: u16,
227 pub zakura_protocol_max: u16,
229 pub network_id: ZakuraNetworkId,
231 pub chain_id: [u8; 32],
233 pub sequence: u64,
235 pub expires_at_unix_secs: u64,
237}
238
239impl ZakuraNodeRecordBody {
240 pub fn encode_for_signature(&self) -> Result<Vec<u8>, DiscoveryWireError> {
242 validate_record_body_bounds(self)?;
243
244 let mut bytes = Vec::new();
245 bytes.write_all(ZAKURA_NODE_RECORD_SIG_DOMAIN)?;
246 bytes.write_u16::<LittleEndian>(ZAKURA_NODE_RECORD_FORMAT_VERSION)?;
247 encode_record_body_to(self, &mut bytes)?;
248 if bytes.len() > MAX_NODE_RECORD_BODY_BYTES {
249 return Err(DiscoveryWireError::OversizedPayload {
250 actual: bytes.len(),
251 max: MAX_NODE_RECORD_BODY_BYTES,
252 });
253 }
254 Ok(bytes)
255 }
256}
257
258#[derive(Clone, Debug, Eq, PartialEq)]
260pub struct ZakuraNodeRecord {
261 pub body: ZakuraNodeRecordBody,
263 pub signature: Signature,
265}
266
267impl ZakuraNodeRecord {
268 pub fn sign(
270 body: ZakuraNodeRecordBody,
271 secret_key: &SecretKey,
272 ) -> Result<Self, DiscoveryWireError> {
273 if body.node_id != secret_key.public() {
274 return Err(DiscoveryWireError::SigningKeyMismatch);
275 }
276 let signing_key = SigningKey::from(secret_key.to_bytes());
277 let signature = signing_key.sign(&body.encode_for_signature()?);
278 Ok(Self { body, signature })
279 }
280
281 pub fn verify(
283 &self,
284 context: &DiscoveryRecordValidationContext,
285 ) -> Result<(), DiscoveryRecordError> {
286 validate_record_body_for_import(&self.body, context)?;
287
288 let verification_key = VerificationKey::try_from(&self.body.node_id.as_bytes()[..])
289 .map_err(|_| DiscoveryRecordError::MalformedNodeId)?;
290 let body_bytes = self
291 .body
292 .encode_for_signature()
293 .map_err(DiscoveryRecordError::Wire)?;
294 verification_key
295 .verify(&self.signature, &body_bytes)
296 .map_err(|_| DiscoveryRecordError::InvalidSignature)
297 }
298}
299
300#[derive(Clone, Debug, Eq, PartialEq)]
302pub struct DiscoveryRecordValidationContext {
303 pub expected_network_id: ZakuraNetworkId,
305 pub expected_chain_id: [u8; 32],
307 pub current_unix_secs: u64,
309 pub supported_protocol_min: u16,
311 pub supported_protocol_max: u16,
313 pub max_record_ttl: Duration,
315 pub clock_skew_tolerance: Duration,
317}
318
319#[derive(Clone, Debug, Eq, PartialEq)]
321pub struct GetServices {
322 pub wanted_services: Vec<ZakuraServiceId>,
324}
325
326#[derive(Clone, Debug, Eq, PartialEq)]
328pub struct Services {
329 pub node_id: NodeId,
331 pub expires_at_unix_secs: u64,
333 pub summaries: Vec<ServiceSummaryEnvelope>,
335}
336
337#[derive(Clone, Debug, Eq, PartialEq)]
339pub struct ServiceSummaryEnvelope {
340 pub service_id: ZakuraServiceId,
342 pub summary_tag: u16,
344 pub summary_bytes: Vec<u8>,
346}
347
348impl ServiceSummaryEnvelope {
349 pub fn header_sync(summary: &HeaderSyncServiceSummary) -> Result<Self, DiscoveryWireError> {
351 let mut summary_bytes = Vec::new();
352 encode_header_sync_summary(summary, &mut summary_bytes)?;
353 Ok(Self {
354 service_id: ZakuraServiceId::header_sync(),
355 summary_tag: SUMMARY_TAG_HEADER_SYNC,
356 summary_bytes,
357 })
358 }
359
360 pub fn discovery(summary: &DiscoveryServiceSummary) -> Result<Self, DiscoveryWireError> {
362 let mut summary_bytes = Vec::new();
363 encode_discovery_summary(summary, &mut summary_bytes)?;
364 Ok(Self {
365 service_id: ZakuraServiceId::discovery(),
366 summary_tag: SUMMARY_TAG_DISCOVERY_V1,
367 summary_bytes,
368 })
369 }
370
371 pub fn block_sync(summary: &BlockSyncServiceSummary) -> Result<Self, DiscoveryWireError> {
373 let mut summary_bytes = Vec::new();
374 encode_block_sync_summary(summary, &mut summary_bytes)?;
375 Ok(Self {
376 service_id: ZakuraServiceId::block_sync(),
377 summary_tag: SUMMARY_TAG_BLOCK_SYNC_V1,
378 summary_bytes,
379 })
380 }
381
382 pub fn decode_header_sync(
384 &self,
385 ) -> Result<Option<HeaderSyncServiceSummary>, DiscoveryWireError> {
386 let is_current_header_summary = self.summary_tag == SUMMARY_TAG_HEADER_SYNC
387 && self.service_id == ZakuraServiceId::header_sync();
388 let is_retired_header_summary = self.summary_tag == SUMMARY_TAG_HEADER_SYNC_RETIRED
389 && self.service_id == ZakuraServiceId::header_sync_retired();
390
391 if !is_current_header_summary && !is_retired_header_summary {
392 return Ok(None);
393 }
394
395 decode_header_sync_summary(&self.summary_bytes).map(Some)
396 }
397
398 pub fn decode_discovery(&self) -> Result<Option<DiscoveryServiceSummary>, DiscoveryWireError> {
400 if self.summary_tag == SUMMARY_TAG_DISCOVERY_V1 {
401 decode_discovery_summary(&self.summary_bytes).map(Some)
402 } else {
403 Ok(None)
404 }
405 }
406
407 pub fn decode_block_sync(&self) -> Result<Option<BlockSyncServiceSummary>, DiscoveryWireError> {
409 if self.summary_tag == SUMMARY_TAG_BLOCK_SYNC_V1 {
410 decode_block_sync_summary(&self.summary_bytes).map(Some)
411 } else {
412 Ok(None)
413 }
414 }
415}
416
417#[derive(Copy, Clone, Debug, Eq, PartialEq)]
419pub struct HeaderSyncServiceSummary {
420 pub best_height: block::Height,
422 pub best_hash: block::Hash,
424 pub finalized_height: Option<block::Height>,
426 pub serving_headers: bool,
428 pub inbound_slots_free: u16,
430 pub inbound_slots_max: u16,
432 pub outbound_slots_free: u16,
434 pub outbound_slots_max: u16,
436}
437
438impl HeaderSyncServiceSummary {
439 pub fn from_snapshot(
441 best_height: block::Height,
442 best_hash: block::Hash,
443 finalized_height: Option<block::Height>,
444 serving_headers: bool,
445 peers: ServicePeerSnapshot,
446 ) -> Self {
447 Self {
448 best_height,
449 best_hash,
450 finalized_height,
451 serving_headers,
452 inbound_slots_free: saturating_u16(peers.inbound_slots_free),
453 inbound_slots_max: saturating_u16(
454 peers.inbound_peers.saturating_add(peers.inbound_slots_free),
455 ),
456 outbound_slots_free: saturating_u16(peers.outbound_slots_free),
457 outbound_slots_max: saturating_u16(
458 peers
459 .outbound_peers
460 .saturating_add(peers.outbound_slots_free),
461 ),
462 }
463 }
464}
465
466#[derive(Copy, Clone, Debug, Eq, PartialEq)]
468pub struct DiscoveryServiceSummary {
469 pub peer_exchange_slots_free: u16,
471 pub max_records_per_response: u16,
473 pub expected_disconnect_after_exchange: bool,
475}
476
477impl DiscoveryServiceSummary {
478 pub fn from_snapshot(
480 peers: ServicePeerSnapshot,
481 max_records_per_response: usize,
482 expected_disconnect_after_exchange: bool,
483 ) -> Self {
484 Self {
485 peer_exchange_slots_free: saturating_u16(peers.inbound_slots_free),
486 max_records_per_response: saturating_u16(
487 max_records_per_response.min(MAX_DISCOVERY_RECORDS_PER_RESPONSE),
488 ),
489 expected_disconnect_after_exchange,
490 }
491 }
492}
493
494#[derive(Copy, Clone, Debug, Eq, PartialEq)]
496pub struct BlockSyncServiceSummary {
497 pub servable_low: block::Height,
499 pub servable_high: block::Height,
501 pub tip_hash: block::Hash,
503 pub free_slots: u16,
505 pub max_blocks_per_response: u32,
507 pub max_response_bytes: u32,
509}
510
511impl BlockSyncServiceSummary {
512 pub fn from_status_and_snapshot(status: BlockSyncStatus, peers: ServicePeerSnapshot) -> Self {
514 Self {
515 servable_low: status.servable_low,
516 servable_high: status.servable_high,
517 tip_hash: status.tip_hash,
518 free_slots: saturating_u16(peers.inbound_slots_free),
519 max_blocks_per_response: status.max_blocks_per_response,
520 max_response_bytes: status.max_response_bytes,
521 }
522 }
523
524 pub fn covers_gap(&self, gap: &[block::Height]) -> bool {
526 self.gap_coverage(gap) == BlockSyncGapCoverage::Whole
527 }
528
529 fn gap_coverage(&self, gap: &[block::Height]) -> BlockSyncGapCoverage {
531 if self.free_slots == 0 {
532 return BlockSyncGapCoverage::None;
533 }
534
535 let Some(first) = gap.iter().min().copied() else {
536 return BlockSyncGapCoverage::None;
537 };
538 let Some(last) = gap.iter().max().copied() else {
539 return BlockSyncGapCoverage::None;
540 };
541 if self.servable_low <= first && last <= self.servable_high {
542 BlockSyncGapCoverage::Whole
543 } else if self.servable_low <= first && first <= self.servable_high {
544 BlockSyncGapCoverage::LowPrefix
545 } else {
546 BlockSyncGapCoverage::None
547 }
548 }
549}
550
551#[derive(Copy, Clone, Debug, Eq, PartialEq)]
552enum BlockSyncGapCoverage {
553 None,
554 LowPrefix,
555 Whole,
556}
557
558#[derive(Clone, Debug, Eq, PartialEq)]
560pub enum DiscoveryMessage {
561 Hello {
563 record: ZakuraNodeRecord,
565 },
566 GetPeers {
568 limit: u16,
570 wanted_services: Vec<ZakuraServiceId>,
572 exclude_node_ids: Vec<NodeId>,
574 },
575 Peers {
577 records: Vec<ZakuraNodeRecord>,
579 },
580 GetServices(GetServices),
582 Services(Services),
584}
585
586impl DiscoveryMessage {
587 pub fn encode(&self) -> Result<Vec<u8>, DiscoveryWireError> {
589 let mut bytes = Vec::new();
590 match self {
591 Self::Hello { record } => {
592 bytes.write_u8(MSG_DISCOVERY_HELLO)?;
593 encode_record(record, &mut bytes)?;
594 }
595 Self::GetPeers {
596 limit,
597 wanted_services,
598 exclude_node_ids,
599 } => {
600 validate_query_fields(*limit, wanted_services, exclude_node_ids)?;
601 bytes.write_u8(MSG_DISCOVERY_GET_PEERS)?;
602 encode_query_fields(*limit, wanted_services, exclude_node_ids, &mut bytes)?;
603 }
604 Self::Peers { records } => {
605 encode_records_message(
606 MSG_DISCOVERY_PEERS,
607 records,
608 MAX_DISCOVERY_RECORDS_PER_RESPONSE,
609 &mut bytes,
610 )?;
611 }
612 Self::GetServices(query) => {
613 validate_get_services(query)?;
614 bytes.write_u8(MSG_DISCOVERY_GET_SERVICES)?;
615 encode_service_ids_capped(
616 &query.wanted_services,
617 MAX_GET_SERVICES_WANTED,
618 &mut bytes,
619 )?;
620 }
621 Self::Services(services) => {
622 validate_services(services)?;
623 bytes.write_u8(MSG_DISCOVERY_SERVICES)?;
624 encode_services_message(services, &mut bytes)?;
625 }
626 }
627
628 if bytes.len() > MAX_DISCOVERY_MESSAGE_BYTES {
629 return Err(DiscoveryWireError::OversizedPayload {
630 actual: bytes.len(),
631 max: MAX_DISCOVERY_MESSAGE_BYTES,
632 });
633 }
634
635 Ok(bytes)
636 }
637
638 pub fn decode(bytes: &[u8]) -> Result<Self, DiscoveryWireError> {
640 if bytes.len() > MAX_DISCOVERY_MESSAGE_BYTES {
641 return Err(DiscoveryWireError::OversizedPayload {
642 actual: bytes.len(),
643 max: MAX_DISCOVERY_MESSAGE_BYTES,
644 });
645 }
646
647 let mut reader = Cursor::new(bytes);
648 let message = match reader.read_u8()? {
649 MSG_DISCOVERY_HELLO => Self::Hello {
650 record: decode_record(&mut reader)?,
651 },
652 MSG_DISCOVERY_GET_PEERS => {
653 let (limit, wanted_services, exclude_node_ids) = decode_query_fields(&mut reader)?;
654 Self::GetPeers {
655 limit,
656 wanted_services,
657 exclude_node_ids,
658 }
659 }
660 MSG_DISCOVERY_PEERS => Self::Peers {
661 records: decode_record_list(&mut reader, MAX_DISCOVERY_RECORDS_PER_RESPONSE)?,
662 },
663 MSG_DISCOVERY_GET_SERVICES => Self::GetServices(decode_get_services(&mut reader)?),
664 MSG_DISCOVERY_SERVICES => Self::Services(decode_services_message(&mut reader)?),
665 value => return Err(DiscoveryWireError::InvalidMessageType(value)),
666 };
667 reject_trailing(bytes, &reader)?;
668 Ok(message)
669 }
670}
671
672#[derive(Error, Debug)]
674pub enum DiscoveryWireError {
675 #[error("Zakura discovery payload length {actual} exceeds hard cap {max}")]
677 OversizedPayload {
678 actual: usize,
680 max: usize,
682 },
683
684 #[error("Zakura discovery wire I/O error: {0}")]
686 Io(#[from] io::Error),
687
688 #[error("Zakura discovery field declared {declared} bytes with only {remaining} remaining")]
690 TruncatedPayload {
691 declared: usize,
693 remaining: usize,
695 },
696
697 #[error("invalid Zakura discovery message type {0}")]
699 InvalidMessageType(u8),
700
701 #[error("invalid Zakura discovery network id {0}")]
703 InvalidNetworkId(u32),
704
705 #[error("invalid Zakura discovery socket address family {0}")]
707 InvalidAddressFamily(u8),
708
709 #[error("invalid Zakura discovery node id")]
711 InvalidNodeId,
712
713 #[error("Zakura discovery {field} does not match authenticated peer node id")]
715 MismatchedNodeId {
716 field: &'static str,
718 },
719
720 #[error("trailing bytes in Zakura discovery payload")]
722 TrailingBytes,
723
724 #[error("empty Zakura discovery {0}")]
726 Empty(&'static str),
727
728 #[error("Zakura discovery service id is not ASCII")]
730 NonAsciiServiceId,
731
732 #[error("invalid Zakura discovery boolean {field} value {value}")]
734 InvalidBoolean {
735 field: &'static str,
737 value: u8,
739 },
740
741 #[error("Zakura discovery summary tag {summary_tag} does not match service id {service_id}")]
743 MismatchedSummaryService {
744 summary_tag: u16,
746 service_id: ZakuraServiceId,
748 },
749
750 #[error("invalid Zakura discovery block height {0}")]
752 InvalidHeight(u32),
753
754 #[error("numeric overflow while encoding Zakura discovery {0}")]
756 NumericOverflow(&'static str),
757
758 #[error("Zakura discovery signing key does not match record node id")]
760 SigningKeyMismatch,
761
762 #[error("invalid Zakura discovery protocol range")]
764 InvalidProtocolRange,
765}
766
767#[derive(Error, Debug)]
769pub enum DiscoveryRecordError {
770 #[error(transparent)]
772 Wire(#[from] DiscoveryWireError),
773
774 #[error("malformed Zakura discovery node id")]
776 MalformedNodeId,
777
778 #[error("invalid Zakura discovery record signature")]
780 InvalidSignature,
781
782 #[error("expired Zakura discovery record")]
784 Expired,
785
786 #[error("far-future Zakura discovery record expiry")]
788 FarFutureExpiry,
789
790 #[error("wrong Zakura discovery network")]
792 WrongNetwork,
793
794 #[error("wrong Zakura discovery chain")]
796 WrongChain,
797
798 #[error("incompatible Zakura discovery protocol range")]
800 IncompatibleProtocol,
801}
802
803pub const DEFAULT_MAX_DISCOVERY_BOOK_RECORDS: usize = 10_000;
805
806const HIGH_DIAL_FAILURE_COUNT: u32 = 3;
807
808#[derive(Copy, Clone, Debug, Eq, PartialEq)]
810pub struct ZakuraDiscoveryBookLimits {
811 pub max_records: usize,
813 pub max_imported_records_per_response: usize,
815 pub max_direct_addrs_per_record: usize,
817 pub max_services_per_record: usize,
819 pub max_encoded_record_bytes: usize,
821}
822
823impl Default for ZakuraDiscoveryBookLimits {
824 fn default() -> Self {
825 Self {
826 max_records: DEFAULT_MAX_DISCOVERY_BOOK_RECORDS,
827 max_imported_records_per_response: MAX_DISCOVERY_RECORDS_PER_RESPONSE,
828 max_direct_addrs_per_record: MAX_DIRECT_ADDRS_PER_RECORD,
829 max_services_per_record: MAX_SERVICES_PER_RECORD,
830 max_encoded_record_bytes: MAX_DISCOVERY_MESSAGE_BYTES,
831 }
832 }
833}
834
835#[derive(Clone, Debug, Eq, PartialEq)]
837pub struct ZakuraDiscoveryEntry {
838 record: ZakuraNodeRecord,
839 source: Option<NodeId>,
840 is_static: bool,
841 last_seen: u64,
842 last_dial_attempt: Option<u64>,
843 last_success: Option<u64>,
844 last_short_lived_exchange: Option<u64>,
845 failure_count: u32,
846}
847
848impl ZakuraDiscoveryEntry {
849 pub fn record(&self) -> &ZakuraNodeRecord {
851 &self.record
852 }
853
854 pub fn source(&self) -> Option<NodeId> {
856 self.source
857 }
858
859 pub fn is_static(&self) -> bool {
861 self.is_static
862 }
863
864 pub fn last_seen(&self) -> u64 {
866 self.last_seen
867 }
868
869 pub fn last_dial_attempt(&self) -> Option<u64> {
871 self.last_dial_attempt
872 }
873
874 pub fn last_success(&self) -> Option<u64> {
876 self.last_success
877 }
878
879 pub fn last_short_lived_exchange(&self) -> Option<u64> {
881 self.last_short_lived_exchange
882 }
883
884 pub fn failure_count(&self) -> u32 {
886 self.failure_count
887 }
888}
889
890#[derive(Clone, Debug, Eq, PartialEq)]
896pub struct ZakuraDiscoveryPersistedEntry {
897 pub record: ZakuraNodeRecord,
899 pub source: Option<NodeId>,
901 pub is_static: bool,
903 pub last_seen: u64,
905 pub last_dial_attempt: Option<u64>,
907 pub last_success: Option<u64>,
909 pub failure_count: u32,
911}
912
913#[derive(Clone, Debug, Eq, PartialEq)]
919pub struct ZakuraDiscoveryDialCandidate {
920 pub node_id: NodeId,
922 pub direct_addrs: Vec<SocketAddr>,
924 pub is_static: bool,
926}
927
928impl ZakuraDiscoveryDialCandidate {
929 pub fn node_addr(&self) -> NodeAddr {
931 NodeAddr::new(self.node_id).with_direct_addresses(self.direct_addrs.clone())
932 }
933}
934
935#[derive(Clone, Debug, Default, Eq, PartialEq)]
937pub struct ZakuraServiceCandidates {
938 pub connected: Vec<NodeId>,
940 pub discovered: Vec<ZakuraDiscoveryDialCandidate>,
942 pub used_fallback: bool,
944}
945
946#[derive(Clone, Debug, Eq, PartialEq)]
948pub struct ZakuraHeaderSyncCandidateState {
949 pub target_height: block::Height,
951 pub admitted_node_ids: Vec<NodeId>,
953 pub backed_off_node_ids: Vec<NodeId>,
955}
956
957impl Default for ZakuraHeaderSyncCandidateState {
958 fn default() -> Self {
959 Self {
960 target_height: block::Height::MIN,
961 admitted_node_ids: Vec::new(),
962 backed_off_node_ids: Vec::new(),
963 }
964 }
965}
966
967#[derive(Clone, Debug, Default, Eq, PartialEq)]
969pub struct ZakuraBlockSyncCandidateState {
970 pub missing_block_bodies: Vec<block::Height>,
972 pub admitted_node_ids: Vec<NodeId>,
974}
975
976#[derive(Copy, Clone, Debug, Eq, PartialEq)]
978pub enum ImportOutcome {
979 Added,
981 Updated,
983 MetadataUpdated,
985 IgnoredOlder,
987}
988
989#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
991pub struct ImportBatchOutcome {
992 pub attempted: usize,
994 pub added: usize,
996 pub updated: usize,
998 pub metadata_updated: usize,
1000 pub ignored_older: usize,
1002 pub rejected: usize,
1004 pub dropped_for_limit: usize,
1006}
1007
1008impl ImportBatchOutcome {
1009 fn record_success(&mut self, outcome: ImportOutcome) {
1010 match outcome {
1011 ImportOutcome::Added => self.added += 1,
1012 ImportOutcome::Updated => self.updated += 1,
1013 ImportOutcome::MetadataUpdated => self.metadata_updated += 1,
1014 ImportOutcome::IgnoredOlder => self.ignored_older += 1,
1015 }
1016 }
1017}
1018
1019#[derive(Error, Debug)]
1021pub enum DiscoveryBookError {
1022 #[error(transparent)]
1024 Record(#[from] DiscoveryRecordError),
1025
1026 #[error("Zakura discovery book rejected local self-record")]
1028 SelfRecord,
1029
1030 #[error("Zakura discovery connected self-record author does not match authenticated peer")]
1032 MismatchedConnectedPeerRecord,
1033
1034 #[error("Zakura discovery record has no usable direct address")]
1036 NoUsableDirectAddress,
1037
1038 #[error("Zakura discovery record contains non-dialable direct address {addr}")]
1040 NonDialableDirectAddress {
1041 addr: SocketAddr,
1043 },
1044
1045 #[error("Zakura discovery record {field} count {actual} exceeds book limit {max}")]
1047 Limit {
1048 field: &'static str,
1050 actual: usize,
1052 max: usize,
1054 },
1055}
1056
1057#[derive(Clone, Debug)]
1059pub struct ZakuraDiscoveryBook {
1060 entries: HashMap<NodeId, ZakuraDiscoveryEntry>,
1061 static_candidates: HashMap<NodeId, ZakuraStaticDiscoveryCandidate>,
1062 limits: ZakuraDiscoveryBookLimits,
1063 local_node_id: Option<NodeId>,
1064}
1065
1066#[derive(Clone, Debug, Eq, PartialEq)]
1068pub struct ZakuraDiscoveryConfig {
1069 pub record_ttl: Duration,
1071 pub refresh_interval: Duration,
1073 pub peer_sample_limit: usize,
1075 pub book_limits: ZakuraDiscoveryBookLimits,
1077 pub max_excluded_node_ids: usize,
1079 pub dial_backoff_base: Duration,
1081 pub dial_backoff_max: Duration,
1083 pub max_concurrent_discovery_dials: usize,
1085 pub discovery_connection_headroom: usize,
1087 pub max_zakura_connections: usize,
1089 pub peer_limits: ServicePeerLimits,
1091 pub max_record_ttl: Duration,
1093 pub clock_skew_tolerance: Duration,
1095}
1096
1097impl Default for ZakuraDiscoveryConfig {
1098 fn default() -> Self {
1099 Self {
1100 record_ttl: DEFAULT_DISCOVERY_RECORD_TTL,
1101 refresh_interval: DEFAULT_DISCOVERY_REFRESH_INTERVAL,
1102 peer_sample_limit: DEFAULT_DISCOVERY_PEER_SAMPLE_LIMIT,
1103 book_limits: ZakuraDiscoveryBookLimits::default(),
1104 max_excluded_node_ids: MAX_DISCOVERY_EXCLUDED_NODE_IDS,
1105 dial_backoff_base: DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE,
1106 dial_backoff_max: DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX,
1107 max_concurrent_discovery_dials: DEFAULT_MAX_CONCURRENT_DISCOVERY_DIALS,
1108 discovery_connection_headroom: DEFAULT_DISCOVERY_CONNECTION_HEADROOM,
1109 max_zakura_connections: DEFAULT_DISCOVERY_ZAKURA_MAX_CONNECTIONS,
1110 peer_limits: ServicePeerLimits::default(),
1111 max_record_ttl: DEFAULT_DISCOVERY_MAX_RECORD_TTL,
1112 clock_skew_tolerance: DEFAULT_DISCOVERY_CLOCK_SKEW_TOLERANCE,
1113 }
1114 }
1115}
1116
1117#[derive(Clone)]
1119pub struct ZakuraDiscoveryLocalConfig {
1120 pub secret_key: SecretKey,
1122 pub direct_addrs: Vec<SocketAddr>,
1124 pub services: Vec<ZakuraServiceId>,
1126 pub zakura_protocol_min: u16,
1128 pub zakura_protocol_max: u16,
1130 pub network_id: ZakuraNetworkId,
1132 pub chain_id: [u8; 32],
1134 pub last_authored_sequence: Option<u64>,
1140}
1141
1142impl fmt::Debug for ZakuraDiscoveryLocalConfig {
1143 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1144 formatter
1145 .debug_struct("ZakuraDiscoveryLocalConfig")
1146 .field("node_id", &self.secret_key.public())
1147 .field("direct_addrs", &self.direct_addrs)
1148 .field("services", &self.services)
1149 .field("zakura_protocol_min", &self.zakura_protocol_min)
1150 .field("zakura_protocol_max", &self.zakura_protocol_max)
1151 .field("network_id", &self.network_id)
1152 .field("chain_id", &self.chain_id)
1153 .field("last_authored_sequence", &self.last_authored_sequence)
1154 .finish()
1155 }
1156}
1157
1158#[derive(Clone)]
1160pub struct ZakuraDiscoveryHandle {
1161 inner: Arc<Mutex<ZakuraDiscoveryInner>>,
1162 connected: watch::Receiver<Vec<ZakuraPeerId>>,
1163 self_record: watch::Receiver<Arc<ZakuraNodeRecord>>,
1164 self_record_tx: watch::Sender<Arc<ZakuraNodeRecord>>,
1165 peer_snapshot: watch::Receiver<ServicePeerSnapshot>,
1166 peer_snapshot_tx: watch::Sender<ServicePeerSnapshot>,
1167 import_validation: DiscoveryImportValidation,
1168}
1169
1170#[derive(Clone, Debug)]
1178struct DiscoveryImportValidation {
1179 expected_network_id: ZakuraNetworkId,
1180 expected_chain_id: [u8; 32],
1181 supported_protocol_min: u16,
1182 supported_protocol_max: u16,
1183 max_record_ttl: Duration,
1184 clock_skew_tolerance: Duration,
1185 max_imported_records_per_response: usize,
1186}
1187
1188impl DiscoveryImportValidation {
1189 fn context(&self, now: u64) -> DiscoveryRecordValidationContext {
1190 DiscoveryRecordValidationContext {
1191 expected_network_id: self.expected_network_id,
1192 expected_chain_id: self.expected_chain_id,
1193 current_unix_secs: now,
1194 supported_protocol_min: self.supported_protocol_min,
1195 supported_protocol_max: self.supported_protocol_max,
1196 max_record_ttl: self.max_record_ttl,
1197 clock_skew_tolerance: self.clock_skew_tolerance,
1198 }
1199 }
1200}
1201
1202impl fmt::Debug for ZakuraDiscoveryHandle {
1203 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1204 formatter
1205 .debug_struct("ZakuraDiscoveryHandle")
1206 .field(
1207 "self_record_node_id",
1208 &self.self_record.borrow().body.node_id,
1209 )
1210 .finish_non_exhaustive()
1211 }
1212}
1213
1214#[derive(Debug)]
1215struct ZakuraDiscoveryInner {
1216 book: ZakuraDiscoveryBook,
1217 active_services: HashMap<NodeId, ZakuraActiveServiceEntry>,
1218 admitted_peers: HashMap<ZakuraPeerId, ZakuraDiscoveryAdmittedPeer>,
1219 last_connected_node_ids: HashSet<NodeId>,
1220 local: ZakuraLocalDiscoveryState,
1221 config: ZakuraDiscoveryConfig,
1222}
1223
1224#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1225struct ZakuraDiscoveryAdmittedPeer {
1226 conn_id: ZakuraConnId,
1227 direction: ServicePeerDirection,
1228}
1229
1230#[derive(Clone, Debug, Eq, PartialEq)]
1231struct ZakuraActiveServiceEntry {
1232 record: Option<ZakuraNodeRecord>,
1233 services: Vec<ZakuraServiceId>,
1234 live_summaries: Vec<ZakuraCachedLiveServiceSummary>,
1235}
1236
1237impl ZakuraActiveServiceEntry {
1238 fn from_record(record: ZakuraNodeRecord, services: Vec<ZakuraServiceId>) -> Self {
1239 Self {
1240 record: Some(record),
1241 services,
1242 live_summaries: Vec::new(),
1243 }
1244 }
1245
1246 fn from_live_summaries(summaries: Vec<ZakuraCachedLiveServiceSummary>) -> Self {
1247 let mut services = Vec::new();
1248 for summary in &summaries {
1249 push_unique_service(&mut services, summary.service_id.clone());
1250 }
1251 Self {
1252 record: None,
1253 services,
1254 live_summaries: summaries,
1255 }
1256 }
1257
1258 fn update_record(&mut self, record: ZakuraNodeRecord, services: Vec<ZakuraServiceId>) {
1259 self.record = Some(record);
1260 self.services = services;
1261 }
1262
1263 fn update_live_summaries(&mut self, summaries: Vec<ZakuraCachedLiveServiceSummary>) {
1264 self.live_summaries.retain(|cached| {
1265 !summaries.iter().any(|summary| {
1266 summary.service_id == cached.service_id && summary.summary_tag == cached.summary_tag
1267 })
1268 });
1269 for summary in summaries {
1270 self.live_summaries.push(summary);
1271 }
1272 if self.record.is_none() {
1273 self.refresh_live_derived_services();
1274 }
1275 }
1276
1277 fn prune_expired_live_summaries(&mut self, now_unix_secs: u64) {
1278 self.live_summaries
1279 .retain(|summary| summary.expires_at_unix_secs > now_unix_secs);
1280 if self.record.is_none() {
1281 self.refresh_live_derived_services();
1282 }
1283 }
1284
1285 fn refresh_live_derived_services(&mut self) {
1286 let mut services = Vec::new();
1287 for summary in &self.live_summaries {
1288 push_unique_service(&mut services, summary.service_id.clone());
1289 }
1290 self.services = services;
1291 }
1292
1293 fn is_empty_live_only(&self) -> bool {
1294 self.record.is_none() && self.live_summaries.is_empty()
1295 }
1296
1297 fn live_summary_preference(&self, service: &ZakuraServiceId, now_unix_secs: u64) -> u32 {
1298 self.live_summaries
1299 .iter()
1300 .filter(|summary| {
1301 self.summary_advises_service(summary, service)
1302 && summary.expires_at_unix_secs > now_unix_secs
1303 })
1304 .map(ZakuraCachedLiveServiceSummary::preference_score)
1305 .max()
1306 .unwrap_or(0)
1307 }
1308
1309 fn fresh_header_sync_summary(&self, now_unix_secs: u64) -> Option<HeaderSyncServiceSummary> {
1310 let header_sync_service = ZakuraServiceId::header_sync();
1311 self.live_summaries.iter().find_map(|summary| {
1312 if !self.summary_advises_service(summary, &header_sync_service)
1313 || summary.expires_at_unix_secs <= now_unix_secs
1314 {
1315 return None;
1316 }
1317 match summary.summary {
1318 ZakuraLiveServiceSummary::HeaderSync(header_sync) => Some(header_sync),
1319 ZakuraLiveServiceSummary::BlockSync(_)
1320 | ZakuraLiveServiceSummary::Discovery(_)
1321 | ZakuraLiveServiceSummary::Unknown => None,
1322 }
1323 })
1324 }
1325
1326 fn summary_advises_service(
1327 &self,
1328 summary: &ZakuraCachedLiveServiceSummary,
1329 service: &ZakuraServiceId,
1330 ) -> bool {
1331 if summary.service_id == *service {
1332 return true;
1333 }
1334
1335 *service == ZakuraServiceId::header_sync()
1336 && summary.service_id == ZakuraServiceId::header_sync_retired()
1337 && self.services.contains(service)
1338 }
1339
1340 fn fresh_block_sync_summary(&self, now_unix_secs: u64) -> Option<BlockSyncServiceSummary> {
1341 self.live_summaries.iter().find_map(|summary| {
1342 if summary.service_id != ZakuraServiceId::block_sync()
1343 || summary.expires_at_unix_secs <= now_unix_secs
1344 {
1345 return None;
1346 }
1347 match summary.summary {
1348 ZakuraLiveServiceSummary::BlockSync(block_sync) => Some(block_sync),
1349 ZakuraLiveServiceSummary::HeaderSync(_)
1350 | ZakuraLiveServiceSummary::Discovery(_)
1351 | ZakuraLiveServiceSummary::Unknown => None,
1352 }
1353 })
1354 }
1355}
1356
1357#[derive(Clone, Debug, Eq, PartialEq)]
1359pub struct ZakuraCachedLiveServiceSummary {
1360 pub service_id: ZakuraServiceId,
1362 pub summary_tag: u16,
1364 pub summary: ZakuraLiveServiceSummary,
1366 pub observed_at_unix_secs: u64,
1368 pub expires_at_unix_secs: u64,
1370}
1371
1372impl ZakuraCachedLiveServiceSummary {
1373 fn from_envelope(
1374 envelope: &ServiceSummaryEnvelope,
1375 observed_at_unix_secs: u64,
1376 expires_at_unix_secs: u64,
1377 ) -> Result<Self, DiscoveryWireError> {
1378 let summary = if let Some(summary) = envelope.decode_header_sync()? {
1379 ZakuraLiveServiceSummary::HeaderSync(summary)
1380 } else if let Some(summary) = envelope.decode_discovery()? {
1381 ZakuraLiveServiceSummary::Discovery(summary)
1382 } else if let Some(summary) = envelope.decode_block_sync()? {
1383 ZakuraLiveServiceSummary::BlockSync(summary)
1384 } else {
1385 ZakuraLiveServiceSummary::Unknown
1386 };
1387 Ok(Self {
1388 service_id: envelope.service_id.clone(),
1389 summary_tag: envelope.summary_tag,
1390 summary,
1391 observed_at_unix_secs,
1392 expires_at_unix_secs,
1393 })
1394 }
1395
1396 fn preference_score(&self) -> u32 {
1397 match self.summary {
1398 ZakuraLiveServiceSummary::HeaderSync(summary) => {
1399 if !summary.serving_headers {
1400 return 0;
1401 }
1402 u32::from(summary.inbound_slots_free)
1403 .saturating_add(u32::from(summary.outbound_slots_free))
1404 }
1405 ZakuraLiveServiceSummary::Discovery(summary) => {
1406 u32::from(summary.peer_exchange_slots_free)
1407 }
1408 ZakuraLiveServiceSummary::BlockSync(summary) => u32::from(summary.free_slots)
1409 .saturating_add(
1410 summary
1411 .max_blocks_per_response
1412 .min(MAX_BS_BLOCKS_PER_REQUEST),
1413 ),
1414 ZakuraLiveServiceSummary::Unknown => 1,
1415 }
1416 }
1417}
1418
1419#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1421pub enum ZakuraLiveServiceSummary {
1422 HeaderSync(HeaderSyncServiceSummary),
1424 Discovery(DiscoveryServiceSummary),
1426 BlockSync(BlockSyncServiceSummary),
1428 Unknown,
1430}
1431
1432#[derive(Clone)]
1433struct ZakuraLocalDiscoveryState {
1434 secret_key: SecretKey,
1435 node_id: NodeId,
1436 direct_addrs: Vec<SocketAddr>,
1437 services: Vec<ZakuraServiceId>,
1438 zakura_protocol_min: u16,
1439 zakura_protocol_max: u16,
1440 network_id: ZakuraNetworkId,
1441 chain_id: [u8; 32],
1442 next_sequence: u64,
1443}
1444
1445impl fmt::Debug for ZakuraLocalDiscoveryState {
1446 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1447 formatter
1448 .debug_struct("ZakuraLocalDiscoveryState")
1449 .field("node_id", &self.node_id)
1450 .field("direct_addrs", &self.direct_addrs)
1451 .field("services", &self.services)
1452 .field("zakura_protocol_min", &self.zakura_protocol_min)
1453 .field("zakura_protocol_max", &self.zakura_protocol_max)
1454 .field("network_id", &self.network_id)
1455 .field("chain_id", &self.chain_id)
1456 .field("next_sequence", &self.next_sequence)
1457 .finish()
1458 }
1459}
1460
1461impl ZakuraDiscoveryHandle {
1462 pub fn new(
1464 local_config: ZakuraDiscoveryLocalConfig,
1465 config: ZakuraDiscoveryConfig,
1466 connected: watch::Receiver<Vec<ZakuraPeerId>>,
1467 ) -> Result<Self, DiscoveryWireError> {
1468 Self::new_at(
1469 local_config,
1470 config,
1471 connected,
1472 current_unix_secs(),
1473 current_sequence_tick(),
1474 )
1475 }
1476
1477 fn new_at(
1478 local_config: ZakuraDiscoveryLocalConfig,
1479 config: ZakuraDiscoveryConfig,
1480 connected: watch::Receiver<Vec<ZakuraPeerId>>,
1481 now_unix_secs: u64,
1482 wall_clock_sequence: u64,
1483 ) -> Result<Self, DiscoveryWireError> {
1484 let mut local = ZakuraLocalDiscoveryState::new(local_config, wall_clock_sequence);
1485 let self_record = Arc::new(local.build_self_record(
1486 now_unix_secs,
1487 wall_clock_sequence,
1488 config.record_ttl,
1489 )?);
1490 let (self_record_tx, self_record_rx) = watch::channel(self_record);
1491 let (peer_snapshot_tx, peer_snapshot_rx) =
1492 watch::channel(ServicePeerSnapshot::new(0, 0, config.peer_limits));
1493 let book = ZakuraDiscoveryBook::with_local_node_id(config.book_limits, local.node_id);
1494 let import_validation = DiscoveryImportValidation {
1495 expected_network_id: local.network_id,
1496 expected_chain_id: local.chain_id,
1497 supported_protocol_min: local.zakura_protocol_min,
1498 supported_protocol_max: local.zakura_protocol_max,
1499 max_record_ttl: config.max_record_ttl,
1500 clock_skew_tolerance: config.clock_skew_tolerance,
1501 max_imported_records_per_response: config.book_limits.max_imported_records_per_response,
1502 };
1503 Ok(Self {
1504 inner: Arc::new(Mutex::new(ZakuraDiscoveryInner {
1505 book,
1506 active_services: HashMap::new(),
1507 admitted_peers: HashMap::new(),
1508 last_connected_node_ids: HashSet::new(),
1509 local,
1510 config,
1511 })),
1512 connected,
1513 self_record: self_record_rx,
1514 self_record_tx,
1515 peer_snapshot: peer_snapshot_rx,
1516 peer_snapshot_tx,
1517 import_validation,
1518 })
1519 }
1520
1521 pub fn current_self_record(&self) -> Arc<ZakuraNodeRecord> {
1523 self.self_record.borrow().clone()
1524 }
1525
1526 pub fn local_node_id(&self) -> NodeId {
1528 self.self_record.borrow().body.node_id
1529 }
1530
1531 pub async fn peer_sample_limit(&self) -> usize {
1533 let inner = self.inner.lock().await;
1534 inner.config.peer_sample_limit
1535 }
1536
1537 pub async fn refresh_interval(&self) -> Duration {
1539 let inner = self.inner.lock().await;
1540 inner.config.refresh_interval
1541 }
1542
1543 pub fn peer_snapshot(&self) -> ServicePeerSnapshot {
1545 *self.peer_snapshot.borrow()
1546 }
1547
1548 pub fn subscribe_peer_snapshot(&self) -> watch::Receiver<ServicePeerSnapshot> {
1550 self.peer_snapshot.clone()
1551 }
1552
1553 pub async fn local_discovery_summary(&self) -> DiscoveryServiceSummary {
1555 DiscoveryServiceSummary::from_snapshot(
1556 self.peer_snapshot(),
1557 self.peer_sample_limit().await,
1558 true,
1559 )
1560 }
1561
1562 pub fn local_services_response(&self, summaries: Vec<ServiceSummaryEnvelope>) -> Services {
1564 Services {
1565 node_id: self.local_node_id(),
1566 expires_at_unix_secs: current_unix_secs()
1567 .saturating_add(DEFAULT_LIVE_SERVICE_SUMMARY_TTL.as_secs()),
1568 summaries,
1569 }
1570 }
1571
1572 pub async fn admit_peer(
1574 &self,
1575 conn_id: ZakuraConnId,
1576 peer_id: ZakuraPeerId,
1577 direction: ServicePeerDirection,
1578 ) -> ServiceAdmissionDecision {
1579 let mut inner = self.inner.lock().await;
1580 if let Some(peer) = inner.admitted_peers.get_mut(&peer_id) {
1581 if conn_id < peer.conn_id {
1582 return ServiceAdmissionDecision::RejectNotUseful;
1583 }
1584
1585 *peer = ZakuraDiscoveryAdmittedPeer { conn_id, direction };
1586 self.publish_peer_snapshot_locked(&inner);
1587 return ServiceAdmissionDecision::Admit;
1588 }
1589
1590 let admitted = inner
1591 .admitted_peers
1592 .values()
1593 .filter(|peer| peer.direction == direction)
1594 .count();
1595 let cap = match direction {
1596 ServicePeerDirection::Inbound => inner.config.peer_limits.max_inbound_peers,
1597 ServicePeerDirection::Outbound => inner.config.peer_limits.max_outbound_peers,
1598 };
1599
1600 if admitted >= cap {
1601 ServiceAdmissionDecision::RejectFull
1602 } else {
1603 inner
1604 .admitted_peers
1605 .insert(peer_id, ZakuraDiscoveryAdmittedPeer { conn_id, direction });
1606 self.publish_peer_snapshot_locked(&inner);
1607 ServiceAdmissionDecision::Admit
1608 }
1609 }
1610
1611 pub async fn remove_peer(&self, peer_id: &ZakuraPeerId, conn_id: ZakuraConnId) {
1613 let mut inner = self.inner.lock().await;
1614 if inner
1615 .admitted_peers
1616 .get(peer_id)
1617 .is_some_and(|peer| peer.conn_id == conn_id)
1618 {
1619 inner.admitted_peers.remove(peer_id);
1620 }
1621 self.publish_peer_snapshot_locked(&inner);
1622 }
1623
1624 fn publish_peer_snapshot_locked(&self, inner: &ZakuraDiscoveryInner) {
1625 let inbound = inner
1626 .admitted_peers
1627 .values()
1628 .filter(|peer| peer.direction == ServicePeerDirection::Inbound)
1629 .count();
1630 let outbound = inner
1631 .admitted_peers
1632 .values()
1633 .filter(|peer| peer.direction == ServicePeerDirection::Outbound)
1634 .count();
1635 let _ = self.peer_snapshot_tx.send(ServicePeerSnapshot::new(
1636 inbound,
1637 outbound,
1638 inner.config.peer_limits,
1639 ));
1640 }
1641
1642 pub async fn peer_sample_exclusions(&self) -> Vec<NodeId> {
1646 let connected = connected_peer_node_ids(&self.connected.borrow());
1647 let inner = self.inner.lock().await;
1648 let mut excluded = Vec::with_capacity(inner.config.max_excluded_node_ids);
1649 push_excluded_node_id(
1650 &mut excluded,
1651 inner.local.node_id,
1652 inner.config.max_excluded_node_ids,
1653 );
1654 for node_id in connected {
1655 push_excluded_node_id(&mut excluded, node_id, inner.config.max_excluded_node_ids);
1656 }
1657 for node_id in inner.book.recent_node_ids(
1658 inner
1659 .config
1660 .max_excluded_node_ids
1661 .saturating_sub(excluded.len()),
1662 ) {
1663 push_excluded_node_id(&mut excluded, node_id, inner.config.max_excluded_node_ids);
1664 }
1665 excluded
1666 }
1667
1668 pub async fn update_advertised_services(
1670 &self,
1671 services: Vec<ZakuraServiceId>,
1672 ) -> Result<Arc<ZakuraNodeRecord>, DiscoveryWireError> {
1673 let services = normalize_services(services);
1674 if self.self_record.borrow().body.services == services {
1675 return Ok(self.current_self_record());
1676 }
1677
1678 let record = {
1679 let mut inner = self.inner.lock().await;
1680 inner.local.services = services;
1681 let record_ttl = inner.config.record_ttl;
1682 Arc::new(inner.local.build_self_record(
1683 current_unix_secs(),
1684 current_sequence_tick(),
1685 record_ttl,
1686 )?)
1687 };
1688 self.self_record_tx.send_replace(record.clone());
1689 Ok(record)
1690 }
1691
1692 pub async fn import_peer_records(
1704 &self,
1705 records: impl IntoIterator<Item = ZakuraNodeRecord>,
1706 source: Option<NodeId>,
1707 ) -> ImportBatchOutcome {
1708 let now = current_unix_secs();
1709 let context = self.import_validation.context(now);
1710 let max_records = self.import_validation.max_imported_records_per_response;
1711
1712 let mut outcome = ImportBatchOutcome::default();
1713 let mut verified = Vec::new();
1714 for record in records {
1715 if outcome.attempted >= max_records {
1716 outcome.dropped_for_limit += 1;
1717 continue;
1718 }
1719 outcome.attempted += 1;
1720 match record.verify(&context) {
1721 Ok(()) => verified.push(record),
1722 Err(_) => outcome.rejected += 1,
1723 }
1724 }
1725
1726 if !verified.is_empty() {
1727 let mut inner = self.inner.lock().await;
1728 inner
1729 .book
1730 .import_pre_verified_records(verified, source, now, &context, &mut outcome);
1731 }
1732
1733 outcome
1734 }
1735
1736 pub async fn import_peer_record(
1738 &self,
1739 record: ZakuraNodeRecord,
1740 source: Option<NodeId>,
1741 ) -> Result<ImportOutcome, DiscoveryBookError> {
1742 let now = current_unix_secs();
1743 let mut inner = self.inner.lock().await;
1744 let context = inner.validation_context(now);
1745 inner.book.import_record(record, source, now, &context)
1746 }
1747
1748 pub async fn import_connected_peer_services(
1753 &self,
1754 services: Services,
1755 peer_node_id: NodeId,
1756 ) -> Result<(), DiscoveryWireError> {
1757 self.import_connected_peer_services_at(services, peer_node_id, current_unix_secs())
1758 .await
1759 }
1760
1761 async fn import_connected_peer_services_at(
1762 &self,
1763 services: Services,
1764 peer_node_id: NodeId,
1765 now: u64,
1766 ) -> Result<(), DiscoveryWireError> {
1767 if services.node_id != peer_node_id {
1768 return Err(DiscoveryWireError::MismatchedNodeId {
1769 field: "services node id",
1770 });
1771 }
1772
1773 let connected_node_ids = connected_peer_node_ids(&self.connected.borrow());
1774 let mut inner = self.inner.lock().await;
1775 inner.sync_active_services(&connected_node_ids, now);
1776 if !connected_node_ids.contains(&peer_node_id) {
1777 return Ok(());
1778 }
1779
1780 let Some(expires_at_unix_secs) =
1781 effective_live_summary_expiry(services.expires_at_unix_secs, now)
1782 else {
1783 if let Some(entry) = inner.active_services.get_mut(&peer_node_id) {
1784 entry.live_summaries.clear();
1785 if entry.record.is_none() {
1786 entry.refresh_live_derived_services();
1787 }
1788 }
1789 inner
1790 .active_services
1791 .retain(|_, entry| !entry.is_empty_live_only());
1792 return Ok(());
1793 };
1794
1795 let mut live_summaries = Vec::with_capacity(services.summaries.len());
1796 for envelope in &services.summaries {
1797 live_summaries.push(ZakuraCachedLiveServiceSummary::from_envelope(
1798 envelope,
1799 now,
1800 expires_at_unix_secs,
1801 )?);
1802 }
1803
1804 inner
1805 .active_services
1806 .entry(peer_node_id)
1807 .and_modify(|entry| entry.update_live_summaries(live_summaries.clone()))
1808 .or_insert_with(|| ZakuraActiveServiceEntry::from_live_summaries(live_summaries));
1809 Ok(())
1810 }
1811
1812 pub async fn import_static_record(
1814 &self,
1815 record: ZakuraNodeRecord,
1816 ) -> Result<ImportOutcome, DiscoveryBookError> {
1817 let now = current_unix_secs();
1818 let mut inner = self.inner.lock().await;
1819 let context = inner.validation_context(now);
1820 inner.book.import_static_record(record, now, &context)
1821 }
1822
1823 pub async fn import_connected_peer_record(
1829 &self,
1830 record: ZakuraNodeRecord,
1831 peer_node_id: NodeId,
1832 ) -> Result<ImportOutcome, DiscoveryBookError> {
1833 if record.body.node_id != peer_node_id {
1834 return Err(DiscoveryBookError::MismatchedConnectedPeerRecord);
1835 }
1836
1837 let now = current_unix_secs();
1838 let services = normalize_services(record.body.services.clone());
1839 let mut inner = self.inner.lock().await;
1840 let context = inner.validation_context(now);
1841 record.verify(&context)?;
1842 if record.body.services.len() > inner.config.book_limits.max_services_per_record {
1843 return Err(DiscoveryBookError::Limit {
1844 field: "service",
1845 actual: record.body.services.len(),
1846 max: inner.config.book_limits.max_services_per_record,
1847 });
1848 }
1849
1850 let import_result =
1851 inner
1852 .book
1853 .import_record(record.clone(), Some(peer_node_id), now, &context);
1854 if import_result.is_ok()
1855 || matches!(import_result, Err(ref error) if is_direct_address_import_error(error))
1856 {
1857 inner.update_active_services_from_connected_record(
1858 peer_node_id,
1859 record,
1860 services,
1861 &import_result,
1862 );
1863 }
1864 import_result
1865 }
1866
1867 pub async fn import_persisted_entries(
1869 &self,
1870 entries: impl IntoIterator<Item = ZakuraDiscoveryPersistedEntry>,
1871 ) -> ImportBatchOutcome {
1872 let now = current_unix_secs();
1873 let mut inner = self.inner.lock().await;
1874 let context = inner.validation_context(now);
1875 inner.book.import_persisted_entries(entries, now, &context)
1876 }
1877
1878 pub async fn insert_static_candidate(
1880 &self,
1881 node_addr: NodeAddr,
1882 ) -> Result<(), DiscoveryBookError> {
1883 let mut inner = self.inner.lock().await;
1884 inner
1885 .book
1886 .insert_static_candidate(node_addr, current_unix_secs())
1887 }
1888
1889 pub async fn sample_peers(
1891 &self,
1892 limit: usize,
1893 wanted_services: &[ZakuraServiceId],
1894 exclude_node_ids: &[NodeId],
1895 ) -> Vec<ZakuraNodeRecord> {
1896 let now = current_unix_secs();
1897 let inner = self.inner.lock().await;
1898 let wanted_services = bounded_services(wanted_services, inner.config.book_limits);
1899 let exclude_node_ids =
1900 bounded_node_ids(exclude_node_ids, inner.config.max_excluded_node_ids);
1901 let mut rng = rand::thread_rng();
1902 inner.book.sample_peers(
1903 limit.min(inner.config.peer_sample_limit),
1904 &wanted_services,
1905 &exclude_node_ids,
1906 now,
1907 &mut rng,
1908 )
1909 }
1910
1911 pub async fn dial_candidates(
1913 &self,
1914 wanted_services: &[ZakuraServiceId],
1915 in_flight_node_ids: &[NodeId],
1916 ) -> Vec<ZakuraDiscoveryDialCandidate> {
1917 let connected = self.connected.borrow().clone();
1918 let (limit, dial_backoff_base, dial_backoff_max, book_limits) = {
1919 let inner = self.inner.lock().await;
1920 (
1921 discovery_dial_slot_limit(
1922 connected.len(),
1923 in_flight_node_ids.len(),
1924 inner.config.max_zakura_connections,
1925 inner.config.discovery_connection_headroom,
1926 inner.config.max_concurrent_discovery_dials,
1927 ),
1928 inner.config.dial_backoff_base,
1929 inner.config.dial_backoff_max,
1930 inner.config.book_limits,
1931 )
1932 };
1933 if limit == 0 {
1934 return Vec::new();
1935 }
1936
1937 let connected_node_ids = connected_peer_node_ids(&connected);
1938 let now = current_unix_secs();
1939 let inner = self.inner.lock().await;
1940 let wanted_services = bounded_services(wanted_services, book_limits);
1941 let mut rng = rand::thread_rng();
1942 inner.book.dial_candidates(
1943 limit,
1944 &wanted_services,
1945 DialCandidateExclusions {
1946 connected_node_ids: &connected_node_ids,
1947 in_flight_node_ids,
1948 },
1949 now,
1950 (dial_backoff_base, dial_backoff_max),
1951 &mut rng,
1952 )
1953 }
1954
1955 pub async fn service_candidates(
1957 &self,
1958 service: &ZakuraServiceId,
1959 allow_fallback: bool,
1960 in_flight_node_ids: &[NodeId],
1961 ) -> ZakuraServiceCandidates {
1962 let connected = self.connected.borrow().clone();
1963 let connected_node_ids = connected_peer_node_ids(&connected);
1964 let (limit, dial_backoff_base, dial_backoff_max, book_limits) = {
1965 let inner = self.inner.lock().await;
1966 (
1967 discovery_dial_slot_limit(
1968 connected.len(),
1969 in_flight_node_ids.len(),
1970 inner.config.max_zakura_connections,
1971 inner.config.discovery_connection_headroom,
1972 inner.config.max_concurrent_discovery_dials,
1973 ),
1974 inner.config.dial_backoff_base,
1975 inner.config.dial_backoff_max,
1976 inner.config.book_limits,
1977 )
1978 };
1979
1980 let now = current_unix_secs();
1981 let mut inner = self.inner.lock().await;
1982 inner.sync_active_services(&connected_node_ids, now);
1983
1984 let mut connected_entries: Vec<_> = inner
1985 .active_services
1986 .iter()
1987 .filter(|(node_id, entry)| {
1988 connected_node_ids.contains(node_id) && entry.services.contains(service)
1989 })
1990 .map(|(node_id, entry)| {
1991 (
1992 *node_id,
1993 entry.live_summary_preference(service, now),
1994 node_id_sort_key(node_id),
1995 )
1996 })
1997 .collect();
1998 connected_entries
1999 .sort_by_key(|(_, preference, sort_key)| (Reverse(*preference), *sort_key));
2000 let connected: Vec<NodeId> = connected_entries
2001 .into_iter()
2002 .map(|(node_id, _, _)| node_id)
2003 .collect();
2004
2005 let wanted_services = bounded_services(std::slice::from_ref(service), book_limits);
2006 let mut rng = rand::thread_rng();
2007 let mut discovered = inner.book.dial_candidates(
2008 limit,
2009 &wanted_services,
2010 DialCandidateExclusions {
2011 connected_node_ids: &connected_node_ids,
2012 in_flight_node_ids,
2013 },
2014 now,
2015 (dial_backoff_base, dial_backoff_max),
2016 &mut rng,
2017 );
2018 let used_fallback = allow_fallback && connected.is_empty() && discovered.is_empty();
2019 if used_fallback {
2020 discovered = inner.book.dial_candidates(
2021 limit,
2022 &[],
2023 DialCandidateExclusions {
2024 connected_node_ids: &connected_node_ids,
2025 in_flight_node_ids,
2026 },
2027 now,
2028 (dial_backoff_base, dial_backoff_max),
2029 &mut rng,
2030 );
2031 }
2032
2033 ZakuraServiceCandidates {
2034 connected,
2035 discovered,
2036 used_fallback,
2037 }
2038 }
2039
2040 pub async fn header_sync_candidates(
2045 &self,
2046 header_sync: &ZakuraHeaderSyncCandidateState,
2047 allow_fallback: bool,
2048 in_flight_node_ids: &[NodeId],
2049 ) -> ZakuraServiceCandidates {
2050 let connected = self.connected.borrow().clone();
2051 let connected_node_ids = connected_peer_node_ids(&connected);
2052 let (limit, dial_backoff_base, dial_backoff_max, book_limits) = {
2053 let inner = self.inner.lock().await;
2054 (
2055 discovery_dial_slot_limit(
2056 connected.len(),
2057 in_flight_node_ids.len(),
2058 inner.config.max_zakura_connections,
2059 inner.config.discovery_connection_headroom,
2060 inner.config.max_concurrent_discovery_dials,
2061 ),
2062 inner.config.dial_backoff_base,
2063 inner.config.dial_backoff_max,
2064 inner.config.book_limits,
2065 )
2066 };
2067
2068 let now = current_unix_secs();
2069 let mut inner = self.inner.lock().await;
2070 inner.sync_active_services(&connected_node_ids, now);
2071
2072 let admitted_node_ids: HashSet<_> = header_sync.admitted_node_ids.iter().copied().collect();
2073 let backed_off_node_ids: HashSet<_> =
2074 header_sync.backed_off_node_ids.iter().copied().collect();
2075 let header_sync_service = ZakuraServiceId::header_sync();
2076 let mut connected_entries: Vec<_> = inner
2077 .active_services
2078 .iter()
2079 .filter_map(|(node_id, entry)| {
2080 let is_admitted = admitted_node_ids.contains(node_id);
2081 if !connected_node_ids.contains(node_id)
2082 || !entry.services.contains(&header_sync_service)
2083 || (backed_off_node_ids.contains(node_id) && !is_admitted)
2084 {
2085 return None;
2086 }
2087
2088 let summary = entry.fresh_header_sync_summary(now);
2089 if summary.is_some_and(|summary| {
2090 summary.serving_headers && summary.inbound_slots_free == 0 && !is_admitted
2091 }) {
2092 return None;
2093 }
2094
2095 Some((
2096 *node_id,
2097 header_sync_candidate_preference(summary, header_sync.target_height),
2098 node_id_sort_key(node_id),
2099 ))
2100 })
2101 .collect();
2102 connected_entries
2103 .sort_by_key(|(_, preference, sort_key)| (Reverse(*preference), *sort_key));
2104 let connected: Vec<NodeId> = connected_entries
2105 .into_iter()
2106 .map(|(node_id, _, _)| node_id)
2107 .collect();
2108
2109 let wanted_services =
2110 bounded_services(std::slice::from_ref(&header_sync_service), book_limits);
2111 let mut excluded_node_ids = Vec::with_capacity(
2112 in_flight_node_ids
2113 .len()
2114 .saturating_add(header_sync.backed_off_node_ids.len()),
2115 );
2116 excluded_node_ids.extend_from_slice(in_flight_node_ids);
2117 excluded_node_ids.extend(
2118 header_sync
2119 .backed_off_node_ids
2120 .iter()
2121 .copied()
2122 .filter(|node_id| !admitted_node_ids.contains(node_id)),
2123 );
2124 let mut rng = rand::thread_rng();
2125 let mut discovered = inner.book.dial_candidates(
2126 limit,
2127 &wanted_services,
2128 DialCandidateExclusions {
2129 connected_node_ids: &connected_node_ids,
2130 in_flight_node_ids: &excluded_node_ids,
2131 },
2132 now,
2133 (dial_backoff_base, dial_backoff_max),
2134 &mut rng,
2135 );
2136 let used_fallback = allow_fallback && connected.is_empty() && discovered.is_empty();
2137 if used_fallback {
2138 discovered = inner.book.dial_candidates(
2139 limit,
2140 &[],
2141 DialCandidateExclusions {
2142 connected_node_ids: &connected_node_ids,
2143 in_flight_node_ids: &excluded_node_ids,
2144 },
2145 now,
2146 (dial_backoff_base, dial_backoff_max),
2147 &mut rng,
2148 );
2149 }
2150
2151 ZakuraServiceCandidates {
2152 connected,
2153 discovered,
2154 used_fallback,
2155 }
2156 }
2157
2158 pub async fn block_sync_candidates(
2163 &self,
2164 block_sync: &ZakuraBlockSyncCandidateState,
2165 allow_fallback: bool,
2166 in_flight_node_ids: &[NodeId],
2167 ) -> ZakuraServiceCandidates {
2168 let connected = self.connected.borrow().clone();
2169 let connected_node_ids = connected_peer_node_ids(&connected);
2170 let (limit, dial_backoff_base, dial_backoff_max, book_limits) = {
2171 let inner = self.inner.lock().await;
2172 (
2173 discovery_dial_slot_limit(
2174 connected.len(),
2175 in_flight_node_ids.len(),
2176 inner.config.max_zakura_connections,
2177 inner.config.discovery_connection_headroom,
2178 inner.config.max_concurrent_discovery_dials,
2179 ),
2180 inner.config.dial_backoff_base,
2181 inner.config.dial_backoff_max,
2182 inner.config.book_limits,
2183 )
2184 };
2185
2186 let now = current_unix_secs();
2187 let mut inner = self.inner.lock().await;
2188 inner.sync_active_services(&connected_node_ids, now);
2189
2190 let admitted_node_ids: HashSet<_> = block_sync.admitted_node_ids.iter().copied().collect();
2191 let block_sync_service = ZakuraServiceId::block_sync();
2192 let mut connected_entries: Vec<_> = inner
2193 .active_services
2194 .iter()
2195 .filter_map(|(node_id, entry)| {
2196 if !connected_node_ids.contains(node_id)
2197 || !entry.services.contains(&block_sync_service)
2198 {
2199 return None;
2200 }
2201
2202 if block_sync.missing_block_bodies.is_empty() {
2203 return Some((
2204 *node_id,
2205 entry.live_summary_preference(&block_sync_service, now),
2206 node_id_sort_key(node_id),
2207 ));
2208 }
2209
2210 let is_admitted = admitted_node_ids.contains(node_id);
2211 let summary = entry.fresh_block_sync_summary(now);
2212 if summary.is_some_and(|summary| summary.free_slots == 0 && !is_admitted) {
2213 return None;
2214 }
2215
2216 Some((
2217 *node_id,
2218 block_sync_candidate_preference(summary, &block_sync.missing_block_bodies),
2219 node_id_sort_key(node_id),
2220 ))
2221 })
2222 .collect();
2223 connected_entries
2224 .sort_by_key(|(_, preference, sort_key)| (Reverse(*preference), *sort_key));
2225 let connected: Vec<NodeId> = connected_entries
2226 .into_iter()
2227 .map(|(node_id, _, _)| node_id)
2228 .collect();
2229
2230 let wanted_services =
2231 bounded_services(std::slice::from_ref(&block_sync_service), book_limits);
2232 let mut rng = rand::thread_rng();
2233 let mut discovered = inner.book.dial_candidates(
2234 limit,
2235 &wanted_services,
2236 DialCandidateExclusions {
2237 connected_node_ids: &connected_node_ids,
2238 in_flight_node_ids,
2239 },
2240 now,
2241 (dial_backoff_base, dial_backoff_max),
2242 &mut rng,
2243 );
2244 let used_fallback = allow_fallback && connected.is_empty() && discovered.is_empty();
2245 if used_fallback {
2246 discovered = inner.book.dial_candidates(
2247 limit,
2248 &[],
2249 DialCandidateExclusions {
2250 connected_node_ids: &connected_node_ids,
2251 in_flight_node_ids,
2252 },
2253 now,
2254 (dial_backoff_base, dial_backoff_max),
2255 &mut rng,
2256 );
2257 }
2258
2259 ZakuraServiceCandidates {
2260 connected,
2261 discovered,
2262 used_fallback,
2263 }
2264 }
2265
2266 pub async fn mark_dial_attempt(&self, node_id: &NodeId) {
2268 let mut inner = self.inner.lock().await;
2269 inner.book.mark_dial_attempt(node_id, current_unix_secs());
2270 }
2271
2272 pub async fn mark_dial_success(&self, node_id: &NodeId) {
2274 let mut inner = self.inner.lock().await;
2275 inner.book.mark_dial_success(node_id, current_unix_secs());
2276 }
2277
2278 pub async fn mark_dial_failure(&self, node_id: &NodeId) {
2280 let mut inner = self.inner.lock().await;
2281 inner.book.mark_dial_failure(node_id, current_unix_secs());
2282 }
2283
2284 pub async fn mark_short_lived_exchange(&self, node_id: &NodeId) {
2286 let mut inner = self.inner.lock().await;
2287 inner
2288 .book
2289 .mark_short_lived_exchange(node_id, current_unix_secs());
2290 }
2291
2292 pub async fn active_services(&self, node_id: NodeId) -> Option<Vec<ZakuraServiceId>> {
2294 let connected = connected_peer_node_ids(&self.connected.borrow());
2295 let now = current_unix_secs();
2296 let mut inner = self.inner.lock().await;
2297 inner.sync_active_services(&connected, now);
2298 if !connected.contains(&node_id) {
2299 return None;
2300 }
2301
2302 inner
2303 .active_services
2304 .get(&node_id)
2305 .map(|entry| entry.services.clone())
2306 }
2307
2308 pub async fn live_service_summaries(
2310 &self,
2311 node_id: NodeId,
2312 ) -> Option<Vec<ZakuraCachedLiveServiceSummary>> {
2313 self.live_service_summaries_at(node_id, current_unix_secs())
2314 .await
2315 }
2316
2317 async fn live_service_summaries_at(
2318 &self,
2319 node_id: NodeId,
2320 now: u64,
2321 ) -> Option<Vec<ZakuraCachedLiveServiceSummary>> {
2322 let connected = connected_peer_node_ids(&self.connected.borrow());
2323 let mut inner = self.inner.lock().await;
2324 inner.sync_active_services(&connected, now);
2325 if !connected.contains(&node_id) {
2326 return None;
2327 }
2328
2329 inner
2330 .active_services
2331 .get(&node_id)
2332 .map(|entry| entry.live_summaries.clone())
2333 }
2334
2335 pub async fn record_for(&self, node_id: NodeId) -> Option<ZakuraNodeRecord> {
2337 let inner = self.inner.lock().await;
2338 inner.book.get(&node_id).map(|entry| entry.record().clone())
2339 }
2340
2341 pub async fn persisted_entries(&self) -> Vec<ZakuraDiscoveryPersistedEntry> {
2343 let inner = self.inner.lock().await;
2344 inner.book.persisted_entries()
2345 }
2346}
2347
2348impl ZakuraDiscoveryInner {
2349 fn validation_context(&self, now: u64) -> DiscoveryRecordValidationContext {
2350 DiscoveryRecordValidationContext {
2351 expected_network_id: self.local.network_id,
2352 expected_chain_id: self.local.chain_id,
2353 current_unix_secs: now,
2354 supported_protocol_min: self.local.zakura_protocol_min,
2355 supported_protocol_max: self.local.zakura_protocol_max,
2356 max_record_ttl: self.config.max_record_ttl,
2357 clock_skew_tolerance: self.config.clock_skew_tolerance,
2358 }
2359 }
2360
2361 fn sync_active_services(&mut self, connected_node_ids: &[NodeId], now_unix_secs: u64) {
2362 let connected_node_ids: HashSet<_> = connected_node_ids.iter().copied().collect();
2363 self.active_services
2364 .retain(|node_id, _| connected_node_ids.contains(node_id));
2365 for entry in self.active_services.values_mut() {
2366 entry.prune_expired_live_summaries(now_unix_secs);
2367 }
2368 self.active_services
2369 .retain(|_, entry| !entry.is_empty_live_only());
2370 self.last_connected_node_ids = connected_node_ids;
2371 }
2372
2373 fn update_active_services_from_connected_record(
2374 &mut self,
2375 node_id: NodeId,
2376 record: ZakuraNodeRecord,
2377 services: Vec<ZakuraServiceId>,
2378 import_result: &Result<ImportOutcome, DiscoveryBookError>,
2379 ) {
2380 if self.active_services.get(&node_id).is_some_and(|active| {
2381 active
2382 .record
2383 .as_ref()
2384 .is_some_and(|active_record| record.body.sequence <= active_record.body.sequence)
2385 }) {
2386 return;
2387 }
2388
2389 let should_update = match import_result {
2390 Ok(ImportOutcome::Added | ImportOutcome::Updated) => true,
2391 Ok(ImportOutcome::MetadataUpdated) => self
2392 .book
2393 .get(&node_id)
2394 .is_some_and(|entry| entry.record() == &record),
2395 Ok(ImportOutcome::IgnoredOlder) => false,
2396 Err(error) if is_direct_address_import_error(error) => self
2397 .book
2398 .get(&node_id)
2399 .is_none_or(|entry| connected_record_is_fresh_for_stored_record(&record, entry)),
2400 Err(_) => false,
2401 };
2402
2403 if should_update {
2404 self.active_services
2405 .entry(node_id)
2406 .and_modify(|entry| entry.update_record(record.clone(), services.clone()))
2407 .or_insert_with(|| ZakuraActiveServiceEntry::from_record(record, services));
2408 }
2409 }
2410}
2411
2412impl ZakuraLocalDiscoveryState {
2413 fn new(config: ZakuraDiscoveryLocalConfig, wall_clock_sequence: u64) -> Self {
2414 let node_id = config.secret_key.public();
2415 let persisted_next_sequence = config
2416 .last_authored_sequence
2417 .map(|sequence| sequence.saturating_add(1))
2418 .unwrap_or(0);
2419 Self {
2420 secret_key: config.secret_key,
2421 node_id,
2422 direct_addrs: normalize_direct_addrs(config.direct_addrs),
2423 services: normalize_services(config.services),
2424 zakura_protocol_min: config.zakura_protocol_min,
2425 zakura_protocol_max: config.zakura_protocol_max,
2426 network_id: config.network_id,
2427 chain_id: config.chain_id,
2428 next_sequence: persisted_next_sequence.max(wall_clock_sequence),
2429 }
2430 }
2431
2432 fn build_self_record(
2433 &mut self,
2434 now_unix_secs: u64,
2435 wall_clock_sequence: u64,
2436 record_ttl: Duration,
2437 ) -> Result<ZakuraNodeRecord, DiscoveryWireError> {
2438 let sequence = self.next_sequence.max(wall_clock_sequence);
2439 self.next_sequence = sequence.saturating_add(1);
2440 ZakuraNodeRecord::sign(
2441 ZakuraNodeRecordBody {
2442 node_id: self.node_id,
2443 direct_addrs: self.direct_addrs.clone(),
2444 services: self.services.clone(),
2445 zakura_protocol_min: self.zakura_protocol_min,
2446 zakura_protocol_max: self.zakura_protocol_max,
2447 network_id: self.network_id,
2448 chain_id: self.chain_id,
2449 sequence,
2450 expires_at_unix_secs: now_unix_secs.saturating_add(record_ttl.as_secs()),
2451 },
2452 &self.secret_key,
2453 )
2454 }
2455}
2456
2457impl Default for ZakuraDiscoveryBook {
2458 fn default() -> Self {
2459 Self::new(ZakuraDiscoveryBookLimits::default())
2460 }
2461}
2462
2463impl ZakuraDiscoveryBook {
2464 pub fn new(limits: ZakuraDiscoveryBookLimits) -> Self {
2466 Self {
2467 entries: HashMap::new(),
2468 static_candidates: HashMap::new(),
2469 limits,
2470 local_node_id: None,
2471 }
2472 }
2473
2474 pub fn with_local_node_id(limits: ZakuraDiscoveryBookLimits, local_node_id: NodeId) -> Self {
2476 Self {
2477 entries: HashMap::new(),
2478 static_candidates: HashMap::new(),
2479 limits,
2480 local_node_id: Some(local_node_id),
2481 }
2482 }
2483
2484 pub fn limits(&self) -> ZakuraDiscoveryBookLimits {
2486 self.limits
2487 }
2488
2489 pub fn len(&self) -> usize {
2491 self.entries.len()
2492 + self
2493 .static_candidates
2494 .keys()
2495 .filter(|node_id| !self.entries.contains_key(*node_id))
2496 .count()
2497 }
2498
2499 pub fn is_empty(&self) -> bool {
2501 self.entries.is_empty()
2502 }
2503
2504 pub fn discovered_len(&self) -> usize {
2506 self.entries
2507 .values()
2508 .filter(|entry| !entry.is_static)
2509 .count()
2510 }
2511
2512 pub fn get(&self, node_id: &NodeId) -> Option<&ZakuraDiscoveryEntry> {
2514 self.entries.get(node_id)
2515 }
2516
2517 pub fn insert_static_candidate(
2522 &mut self,
2523 node_addr: NodeAddr,
2524 now: u64,
2525 ) -> Result<(), DiscoveryBookError> {
2526 if self.local_node_id == Some(node_addr.node_id) {
2527 return Err(DiscoveryBookError::SelfRecord);
2528 }
2529
2530 let mut direct_addrs: Vec<_> = node_addr.direct_addresses().copied().collect();
2531 direct_addrs.sort_unstable();
2532 direct_addrs.dedup();
2533 if direct_addrs.is_empty() {
2534 return Err(DiscoveryBookError::NoUsableDirectAddress);
2535 }
2536 for addr in &direct_addrs {
2537 if !is_static_discovery_configured_addr_usable(addr) {
2538 return Err(DiscoveryBookError::NonDialableDirectAddress { addr: *addr });
2539 }
2540 }
2541
2542 let candidate = self
2543 .static_candidates
2544 .entry(node_addr.node_id)
2545 .or_insert_with(|| ZakuraStaticDiscoveryCandidate {
2546 node_id: node_addr.node_id,
2547 direct_addrs: Vec::new(),
2548 last_seen: now,
2549 last_dial_attempt: None,
2550 last_success: None,
2551 last_short_lived_exchange: None,
2552 failure_count: 0,
2553 });
2554 candidate.direct_addrs = direct_addrs;
2555 candidate.last_seen = now;
2556 Ok(())
2557 }
2558
2559 pub fn import_record(
2561 &mut self,
2562 record: ZakuraNodeRecord,
2563 source: Option<NodeId>,
2564 now: u64,
2565 context: &DiscoveryRecordValidationContext,
2566 ) -> Result<ImportOutcome, DiscoveryBookError> {
2567 self.import_record_inner(record, source, false, false, now, context)
2568 }
2569
2570 pub fn import_static_record(
2577 &mut self,
2578 record: ZakuraNodeRecord,
2579 now: u64,
2580 context: &DiscoveryRecordValidationContext,
2581 ) -> Result<ImportOutcome, DiscoveryBookError> {
2582 self.import_record_inner(record, None, true, false, now, context)
2583 }
2584
2585 fn import_pre_verified_records(
2593 &mut self,
2594 records: impl IntoIterator<Item = ZakuraNodeRecord>,
2595 source: Option<NodeId>,
2596 now: u64,
2597 context: &DiscoveryRecordValidationContext,
2598 outcome: &mut ImportBatchOutcome,
2599 ) {
2600 for record in records {
2601 match self.import_record_inner(record, source, false, true, now, context) {
2602 Ok(import_outcome) => outcome.record_success(import_outcome),
2603 Err(_) => outcome.rejected += 1,
2604 }
2605 }
2606 }
2607
2608 pub fn import_records(
2610 &mut self,
2611 records: impl IntoIterator<Item = ZakuraNodeRecord>,
2612 source: Option<NodeId>,
2613 now: u64,
2614 context: &DiscoveryRecordValidationContext,
2615 ) -> ImportBatchOutcome {
2616 let mut outcome = ImportBatchOutcome::default();
2617 for (index, record) in records.into_iter().enumerate() {
2618 if index >= self.limits.max_imported_records_per_response {
2619 outcome.dropped_for_limit += 1;
2620 continue;
2621 }
2622 outcome.attempted += 1;
2623 match self.import_record(record, source, now, context) {
2624 Ok(import_outcome) => outcome.record_success(import_outcome),
2625 Err(_) => outcome.rejected += 1,
2626 }
2627 }
2628 outcome
2629 }
2630
2631 pub fn sample_peers<R: rand::Rng + ?Sized>(
2633 &self,
2634 limit: usize,
2635 wanted_services: &[ZakuraServiceId],
2636 exclude_node_ids: &[NodeId],
2637 now: u64,
2638 rng: &mut R,
2639 ) -> Vec<ZakuraNodeRecord> {
2640 let exclude_node_ids: HashSet<_> = exclude_node_ids.iter().copied().collect();
2641 let limit = limit.min(self.limits.max_imported_records_per_response);
2642
2643 self.entries
2650 .iter()
2651 .filter(|(node_id, entry)| {
2652 !exclude_node_ids.contains(*node_id)
2653 && self.local_node_id != Some(**node_id)
2654 && !entry_is_expired(entry, now)
2655 && has_wanted_services(&entry.record, wanted_services)
2656 && has_discovery_dialable_direct_addrs(&entry.record)
2657 })
2658 .map(|(_, entry)| &entry.record)
2659 .choose_multiple(rng, limit)
2660 .into_iter()
2661 .cloned()
2662 .collect()
2663 }
2664
2665 pub(crate) fn dial_candidates<R: rand::Rng + ?Sized>(
2667 &self,
2668 limit: usize,
2669 wanted_services: &[ZakuraServiceId],
2670 exclusions: DialCandidateExclusions<'_>,
2671 now: u64,
2672 dial_backoff: (Duration, Duration),
2673 rng: &mut R,
2674 ) -> Vec<ZakuraDiscoveryDialCandidate> {
2675 let connected_node_ids: HashSet<_> =
2676 exclusions.connected_node_ids.iter().copied().collect();
2677 let in_flight_node_ids: HashSet<_> =
2678 exclusions.in_flight_node_ids.iter().copied().collect();
2679 let candidates: Vec<_> = self
2680 .entries
2681 .values()
2682 .filter(|entry| {
2683 !connected_node_ids.contains(&entry.record.body.node_id)
2684 && !in_flight_node_ids.contains(&entry.record.body.node_id)
2685 && self.local_node_id != Some(entry.record.body.node_id)
2686 && !entry_is_expired(entry, now)
2687 && !entry_in_dial_backoff(entry, now, dial_backoff.0, dial_backoff.1)
2688 && !entry_in_short_lived_exchange_backoff(
2689 entry,
2690 now,
2691 dial_backoff.0,
2692 dial_backoff.1,
2693 )
2694 && entry_has_confirmed_dial_authority(entry)
2695 && has_wanted_services(&entry.record, wanted_services)
2696 && has_discovery_usable_direct_addrs(entry)
2697 })
2698 .map(DialCandidateRef::SignedRecord)
2699 .chain(self.static_candidates.values().filter_map(|candidate| {
2700 if !wanted_services.is_empty()
2701 || connected_node_ids.contains(&candidate.node_id)
2702 || in_flight_node_ids.contains(&candidate.node_id)
2703 || self.local_node_id == Some(candidate.node_id)
2704 || self.entries.contains_key(&candidate.node_id)
2705 || entry_metadata_in_dial_backoff(
2706 candidate.last_dial_attempt,
2707 candidate.failure_count,
2708 now,
2709 dial_backoff.0,
2710 dial_backoff.1,
2711 )
2712 || entry_metadata_in_short_lived_exchange_backoff(
2713 candidate.last_short_lived_exchange,
2714 now,
2715 dial_backoff.0,
2716 dial_backoff.1,
2717 )
2718 {
2719 return None;
2720 }
2721 Some(DialCandidateRef::StaticConfigured(candidate))
2722 }))
2723 .collect();
2724
2725 let mut keyed: Vec<(DialCandidateSortKey, DialCandidateRef<'_>)> = candidates
2733 .into_iter()
2734 .map(|candidate| (dial_candidate_sort_key(&candidate, rng), candidate))
2735 .collect();
2736
2737 let take = limit.min(keyed.len());
2738 if take < keyed.len() {
2739 keyed.select_nth_unstable_by(take, |a, b| a.0.cmp(&b.0));
2740 keyed.truncate(take);
2741 }
2742 keyed.sort_by_key(|a| a.0);
2743
2744 keyed
2745 .into_iter()
2746 .map(|(_, candidate)| candidate.into_candidate())
2747 .collect()
2748 }
2749
2750 pub fn mark_dial_attempt(&mut self, node_id: &NodeId, now: u64) {
2752 if let Some(entry) = self.entries.get_mut(node_id) {
2753 entry.last_dial_attempt = Some(now);
2754 }
2755 if let Some(candidate) = self.static_candidates.get_mut(node_id) {
2756 candidate.last_dial_attempt = Some(now);
2757 }
2758 }
2759
2760 pub fn mark_dial_success(&mut self, node_id: &NodeId, now: u64) {
2762 if let Some(entry) = self.entries.get_mut(node_id) {
2763 entry.last_success = Some(now);
2764 entry.failure_count = 0;
2765 }
2766 if let Some(candidate) = self.static_candidates.get_mut(node_id) {
2767 candidate.last_success = Some(now);
2768 candidate.failure_count = 0;
2769 }
2770 }
2771
2772 pub fn mark_dial_failure(&mut self, node_id: &NodeId, _now: u64) {
2774 if let Some(entry) = self.entries.get_mut(node_id) {
2775 entry.failure_count = entry.failure_count.saturating_add(1);
2776 }
2777 if let Some(candidate) = self.static_candidates.get_mut(node_id) {
2778 candidate.failure_count = candidate.failure_count.saturating_add(1);
2779 }
2780 }
2781
2782 pub fn mark_short_lived_exchange(&mut self, node_id: &NodeId, now: u64) {
2784 if let Some(entry) = self.entries.get_mut(node_id) {
2785 entry.last_short_lived_exchange = Some(now);
2786 }
2787 if let Some(candidate) = self.static_candidates.get_mut(node_id) {
2788 candidate.last_short_lived_exchange = Some(now);
2789 }
2790 }
2791
2792 pub fn services_for(&self, node_id: &NodeId) -> Option<Vec<ZakuraServiceId>> {
2794 self.entries
2795 .get(node_id)
2796 .map(|entry| entry.record.body.services.clone())
2797 }
2798
2799 fn recent_node_ids(&self, limit: usize) -> Vec<NodeId> {
2800 let mut entries: Vec<_> = self.entries.values().collect();
2801 entries.sort_by_key(|entry| {
2802 (
2803 Reverse(entry.last_seen),
2804 node_id_sort_key(&entry.record.body.node_id),
2805 )
2806 });
2807 entries
2808 .into_iter()
2809 .take(limit)
2810 .map(|entry| entry.record.body.node_id)
2811 .collect()
2812 }
2813
2814 pub fn persisted_entries(&self) -> Vec<ZakuraDiscoveryPersistedEntry> {
2816 let mut entries: Vec<_> = self
2817 .entries
2818 .values()
2819 .map(|entry| ZakuraDiscoveryPersistedEntry {
2820 record: entry.record.clone(),
2821 source: entry.source,
2822 is_static: entry.is_static,
2823 last_seen: entry.last_seen,
2824 last_dial_attempt: entry.last_dial_attempt,
2825 last_success: entry.last_success,
2826 failure_count: entry.failure_count,
2827 })
2828 .collect();
2829 entries.sort_by_key(|entry| node_id_sort_key(&entry.record.body.node_id));
2830 entries
2831 }
2832
2833 pub fn import_persisted_entries(
2835 &mut self,
2836 entries: impl IntoIterator<Item = ZakuraDiscoveryPersistedEntry>,
2837 now: u64,
2838 context: &DiscoveryRecordValidationContext,
2839 ) -> ImportBatchOutcome {
2840 let mut outcome = ImportBatchOutcome::default();
2841 for entry in entries {
2842 outcome.attempted += 1;
2843 match self.import_persisted_entry(entry, now, context) {
2844 Ok(import_outcome) => outcome.record_success(import_outcome),
2845 Err(_) => outcome.rejected += 1,
2846 }
2847 }
2848 outcome
2849 }
2850
2851 fn import_persisted_entry(
2852 &mut self,
2853 entry: ZakuraDiscoveryPersistedEntry,
2854 now: u64,
2855 context: &DiscoveryRecordValidationContext,
2856 ) -> Result<ImportOutcome, DiscoveryBookError> {
2857 let metadata = DiscoveryEntryMetadata {
2858 source: entry.source,
2859 is_static: entry.is_static,
2860 last_seen: entry.last_seen,
2861 last_dial_attempt: entry.last_dial_attempt,
2862 last_success: entry.last_success,
2863 failure_count: entry.failure_count,
2864 };
2865 self.import_validated_record(entry.record, metadata, false, now, context)
2866 }
2867
2868 fn import_record_inner(
2869 &mut self,
2870 record: ZakuraNodeRecord,
2871 source: Option<NodeId>,
2872 is_static: bool,
2873 pre_verified: bool,
2874 now: u64,
2875 context: &DiscoveryRecordValidationContext,
2876 ) -> Result<ImportOutcome, DiscoveryBookError> {
2877 let metadata = DiscoveryEntryMetadata {
2878 source,
2879 is_static,
2880 last_seen: now,
2881 last_dial_attempt: None,
2882 last_success: None,
2883 failure_count: 0,
2884 };
2885 self.import_validated_record(record, metadata, pre_verified, now, context)
2886 }
2887
2888 fn import_validated_record(
2895 &mut self,
2896 record: ZakuraNodeRecord,
2897 metadata: DiscoveryEntryMetadata,
2898 pre_verified: bool,
2899 now: u64,
2900 context: &DiscoveryRecordValidationContext,
2901 ) -> Result<ImportOutcome, DiscoveryBookError> {
2902 if self.local_node_id == Some(record.body.node_id) {
2903 return Err(DiscoveryBookError::SelfRecord);
2904 }
2905
2906 if !pre_verified {
2907 record.verify(context)?;
2908 }
2909 self.validate_record_storage_limits(&record)?;
2910 let direct_addr_policy = if metadata.is_static {
2911 DiscoveryDirectAddrPolicy::StaticConfigured
2912 } else {
2913 DiscoveryDirectAddrPolicy::UntrustedPeer
2914 };
2915 validate_discovery_direct_addrs(&record, direct_addr_policy)?;
2916
2917 let node_id = record.body.node_id;
2918 if let Some(entry) = self.entries.get_mut(&node_id) {
2919 return Ok(update_existing_entry(entry, record, metadata));
2920 }
2921
2922 self.entries.insert(
2923 node_id,
2924 ZakuraDiscoveryEntry {
2925 record,
2926 source: metadata.source,
2927 is_static: metadata.is_static,
2928 last_seen: metadata.last_seen,
2929 last_dial_attempt: metadata.last_dial_attempt,
2930 last_success: metadata.last_success,
2931 last_short_lived_exchange: None,
2932 failure_count: metadata.failure_count,
2933 },
2934 );
2935 self.evict_to_limits(now);
2936 Ok(ImportOutcome::Added)
2937 }
2938
2939 fn validate_record_storage_limits(
2940 &self,
2941 record: &ZakuraNodeRecord,
2942 ) -> Result<(), DiscoveryBookError> {
2943 if record.body.direct_addrs.len() > self.limits.max_direct_addrs_per_record {
2944 return Err(DiscoveryBookError::Limit {
2945 field: "direct address",
2946 actual: record.body.direct_addrs.len(),
2947 max: self.limits.max_direct_addrs_per_record,
2948 });
2949 }
2950 if record.body.services.len() > self.limits.max_services_per_record {
2951 return Err(DiscoveryBookError::Limit {
2952 field: "service",
2953 actual: record.body.services.len(),
2954 max: self.limits.max_services_per_record,
2955 });
2956 }
2957
2958 let mut encoded = Vec::new();
2959 encode_record(record, &mut encoded).map_err(DiscoveryRecordError::from)?;
2960 if encoded.len() > self.limits.max_encoded_record_bytes {
2961 return Err(DiscoveryBookError::Limit {
2962 field: "encoded record byte",
2963 actual: encoded.len(),
2964 max: self.limits.max_encoded_record_bytes,
2965 });
2966 }
2967
2968 Ok(())
2969 }
2970
2971 fn evict_to_limits(&mut self, now: u64) {
2972 while self.discovered_len() > self.limits.max_records {
2973 let Some(node_id) = self.next_eviction_candidate(now) else {
2974 break;
2975 };
2976 self.entries.remove(&node_id);
2977 }
2978 }
2979
2980 fn next_eviction_candidate(&self, now: u64) -> Option<NodeId> {
2981 self.entries
2982 .iter()
2983 .filter(|(_, entry)| !entry.is_static && entry_is_expired(entry, now))
2984 .min_by_key(|(node_id, entry)| {
2985 (
2986 entry.record.body.expires_at_unix_secs,
2987 node_id_sort_key(node_id),
2988 )
2989 })
2990 .map(|(node_id, _)| *node_id)
2991 .or_else(|| {
2992 self.entries
2993 .iter()
2994 .filter(|(_, entry)| {
2995 !entry.is_static
2996 && entry.last_success.is_none()
2997 && entry.failure_count >= HIGH_DIAL_FAILURE_COUNT
2998 })
2999 .max_by_key(|(node_id, entry)| {
3000 (
3001 entry.failure_count,
3002 Reverse(entry.last_seen),
3003 Reverse(node_id_sort_key(node_id)),
3004 )
3005 })
3006 .map(|(node_id, _)| *node_id)
3007 })
3008 .or_else(|| {
3009 self.entries
3010 .iter()
3011 .filter(|(_, entry)| !entry.is_static)
3012 .min_by_key(|(node_id, entry)| {
3013 (
3014 entry.last_success.unwrap_or(0),
3015 entry.last_seen,
3016 node_id_sort_key(node_id),
3017 )
3018 })
3019 .map(|(node_id, _)| *node_id)
3020 })
3021 }
3022}
3023
3024#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3025struct DiscoveryEntryMetadata {
3026 source: Option<NodeId>,
3027 is_static: bool,
3028 last_seen: u64,
3029 last_dial_attempt: Option<u64>,
3030 last_success: Option<u64>,
3031 failure_count: u32,
3032}
3033
3034#[derive(Clone, Debug, Eq, PartialEq)]
3035struct ZakuraStaticDiscoveryCandidate {
3036 node_id: NodeId,
3037 direct_addrs: Vec<SocketAddr>,
3038 last_seen: u64,
3039 last_dial_attempt: Option<u64>,
3040 last_success: Option<u64>,
3041 last_short_lived_exchange: Option<u64>,
3042 failure_count: u32,
3043}
3044
3045#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3046pub(crate) struct DialCandidateExclusions<'a> {
3047 connected_node_ids: &'a [NodeId],
3048 in_flight_node_ids: &'a [NodeId],
3049}
3050
3051#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3052enum DiscoveryDirectAddrPolicy {
3053 UntrustedPeer,
3054 StaticConfigured,
3055}
3056
3057enum DialCandidateRef<'a> {
3058 SignedRecord(&'a ZakuraDiscoveryEntry),
3059 StaticConfigured(&'a ZakuraStaticDiscoveryCandidate),
3060}
3061
3062impl DialCandidateRef<'_> {
3063 fn node_id(&self) -> NodeId {
3064 match self {
3065 Self::SignedRecord(entry) => entry.record.body.node_id,
3066 Self::StaticConfigured(candidate) => candidate.node_id,
3067 }
3068 }
3069
3070 fn is_static(&self) -> bool {
3071 match self {
3072 Self::SignedRecord(entry) => entry.is_static,
3073 Self::StaticConfigured(_) => true,
3074 }
3075 }
3076
3077 fn last_seen(&self) -> u64 {
3078 match self {
3079 Self::SignedRecord(entry) => entry.last_seen,
3080 Self::StaticConfigured(candidate) => candidate.last_seen,
3081 }
3082 }
3083
3084 fn last_success(&self) -> Option<u64> {
3085 match self {
3086 Self::SignedRecord(entry) => entry.last_success,
3087 Self::StaticConfigured(candidate) => candidate.last_success,
3088 }
3089 }
3090
3091 fn failure_count(&self) -> u32 {
3092 match self {
3093 Self::SignedRecord(entry) => entry.failure_count,
3094 Self::StaticConfigured(candidate) => candidate.failure_count,
3095 }
3096 }
3097
3098 fn into_candidate(self) -> ZakuraDiscoveryDialCandidate {
3099 match self {
3100 Self::SignedRecord(entry) => ZakuraDiscoveryDialCandidate {
3101 node_id: entry.record.body.node_id,
3102 direct_addrs: entry.record.body.direct_addrs.clone(),
3103 is_static: entry.is_static,
3104 },
3105 Self::StaticConfigured(candidate) => ZakuraDiscoveryDialCandidate {
3106 node_id: candidate.node_id,
3107 direct_addrs: candidate.direct_addrs.clone(),
3108 is_static: true,
3109 },
3110 }
3111 }
3112}
3113
3114type DialCandidateSortKey = (
3118 bool,
3119 Reverse<u64>,
3120 u32,
3121 Reverse<u64>,
3122 u64,
3123 [u8; NODE_ID_BYTES],
3124);
3125
3126fn dial_candidate_sort_key<R: rand::Rng + ?Sized>(
3133 candidate: &DialCandidateRef<'_>,
3134 rng: &mut R,
3135) -> DialCandidateSortKey {
3136 let non_static_random_tie = if !candidate.is_static() {
3137 rng.gen::<u64>()
3138 } else {
3139 0
3140 };
3141 let static_deterministic_tie = if candidate.is_static() {
3142 node_id_sort_key(&candidate.node_id())
3143 } else {
3144 [0; NODE_ID_BYTES]
3145 };
3146 (
3147 !candidate.is_static(),
3148 Reverse(candidate.last_success().unwrap_or(0)),
3149 candidate.failure_count(),
3150 Reverse(candidate.last_seen()),
3151 non_static_random_tie,
3152 static_deterministic_tie,
3153 )
3154}
3155
3156fn update_existing_entry(
3157 entry: &mut ZakuraDiscoveryEntry,
3158 record: ZakuraNodeRecord,
3159 metadata: DiscoveryEntryMetadata,
3160) -> ImportOutcome {
3161 let incoming_sequence = record.body.sequence;
3162 let stored_sequence = entry.record.body.sequence;
3163
3164 if incoming_sequence < stored_sequence {
3165 return ImportOutcome::IgnoredOlder;
3166 }
3167
3168 let preserve_first_party_source = incoming_sequence == stored_sequence
3169 && entry.source == Some(entry.record.body.node_id)
3170 && metadata.source != Some(entry.record.body.node_id);
3171
3172 if !preserve_first_party_source {
3173 entry.source = metadata.source;
3174 }
3175 entry.is_static |= metadata.is_static;
3176 entry.last_seen = metadata.last_seen;
3177
3178 if incoming_sequence == stored_sequence {
3179 return ImportOutcome::MetadataUpdated;
3180 }
3181
3182 entry.record = record;
3183 ImportOutcome::Updated
3184}
3185
3186fn connected_record_is_fresh_for_stored_record(
3187 record: &ZakuraNodeRecord,
3188 entry: &ZakuraDiscoveryEntry,
3189) -> bool {
3190 match record.body.sequence.cmp(&entry.record.body.sequence) {
3191 std::cmp::Ordering::Less => false,
3192 std::cmp::Ordering::Equal => entry.record() == record,
3193 std::cmp::Ordering::Greater => true,
3194 }
3195}
3196
3197fn is_direct_address_import_error(error: &DiscoveryBookError) -> bool {
3198 matches!(
3199 error,
3200 DiscoveryBookError::NoUsableDirectAddress
3201 | DiscoveryBookError::NonDialableDirectAddress { .. }
3202 )
3203}
3204
3205fn current_unix_secs() -> u64 {
3206 SystemTime::now()
3207 .duration_since(UNIX_EPOCH)
3208 .map(|duration| duration.as_secs())
3209 .unwrap_or(0)
3210}
3211
3212fn current_sequence_tick() -> u64 {
3218 SystemTime::now()
3219 .duration_since(UNIX_EPOCH)
3220 .map(|duration| u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX))
3221 .unwrap_or(0)
3222}
3223
3224fn normalize_direct_addrs(addrs: Vec<SocketAddr>) -> Vec<SocketAddr> {
3225 let mut addrs: Vec<_> = addrs
3226 .into_iter()
3227 .filter(is_discovery_dialable_addr)
3228 .collect();
3229 addrs.sort_unstable();
3230 addrs.dedup();
3231 addrs.truncate(MAX_DIRECT_ADDRS_PER_RECORD);
3232 addrs
3233}
3234
3235fn normalize_services(services: Vec<ZakuraServiceId>) -> Vec<ZakuraServiceId> {
3236 let mut services = services;
3237 services.sort_unstable();
3238 services.dedup();
3239 services.truncate(MAX_SERVICES_PER_RECORD);
3240 services
3241}
3242
3243fn push_unique_service(services: &mut Vec<ZakuraServiceId>, service: ZakuraServiceId) {
3244 if services.iter().any(|existing| existing == &service) {
3245 return;
3246 }
3247 if services.len() < MAX_SERVICES_PER_RECORD {
3248 services.push(service);
3249 services.sort_unstable();
3250 }
3251}
3252
3253fn effective_live_summary_expiry(declared_expires_at: u64, now_unix_secs: u64) -> Option<u64> {
3254 if declared_expires_at <= now_unix_secs {
3255 return None;
3256 }
3257 Some(
3258 declared_expires_at
3259 .min(now_unix_secs.saturating_add(DEFAULT_LIVE_SERVICE_SUMMARY_TTL.as_secs())),
3260 )
3261}
3262
3263fn header_sync_candidate_preference(
3264 summary: Option<HeaderSyncServiceSummary>,
3265 target_height: block::Height,
3266) -> u32 {
3267 let Some(summary) = summary else {
3268 return 0;
3269 };
3270 if !summary.serving_headers
3271 || summary.inbound_slots_free == 0
3272 || summary.best_height < target_height
3273 {
3274 return 0;
3275 }
3276
3277 let useful_height_delta = summary.best_height.0.saturating_sub(target_height.0);
3278 1_000_000u32
3279 .saturating_add(u32::from(summary.inbound_slots_free))
3280 .saturating_add(u32::from(summary.outbound_slots_free))
3281 .saturating_add(useful_height_delta)
3282}
3283
3284fn block_sync_candidate_preference(
3285 summary: Option<BlockSyncServiceSummary>,
3286 missing_block_bodies: &[block::Height],
3287) -> u32 {
3288 let Some(summary) = summary else {
3289 return 0;
3290 };
3291
3292 let coverage_bonus = match summary.gap_coverage(missing_block_bodies) {
3293 BlockSyncGapCoverage::Whole => 1_000_000u32,
3294 BlockSyncGapCoverage::LowPrefix => 500_000u32,
3295 BlockSyncGapCoverage::None => return 0,
3296 };
3297
3298 coverage_bonus
3299 .saturating_add(u32::from(summary.free_slots))
3300 .saturating_add(
3301 summary
3302 .max_blocks_per_response
3303 .min(MAX_BS_BLOCKS_PER_REQUEST),
3304 )
3305}
3306
3307fn bounded_services(
3308 services: &[ZakuraServiceId],
3309 limits: ZakuraDiscoveryBookLimits,
3310) -> Vec<ZakuraServiceId> {
3311 services
3312 .iter()
3313 .take(limits.max_services_per_record)
3314 .cloned()
3315 .collect()
3316}
3317
3318fn bounded_node_ids(node_ids: &[NodeId], max_node_ids: usize) -> Vec<NodeId> {
3319 node_ids.iter().take(max_node_ids).copied().collect()
3320}
3321
3322fn push_excluded_node_id(excluded: &mut Vec<NodeId>, node_id: NodeId, max_node_ids: usize) {
3323 if excluded.len() < max_node_ids && !excluded.contains(&node_id) {
3324 excluded.push(node_id);
3325 }
3326}
3327
3328fn connected_peer_node_ids(connected: &[ZakuraPeerId]) -> Vec<NodeId> {
3329 connected
3330 .iter()
3331 .filter_map(|peer_id| {
3332 let bytes: [u8; NODE_ID_BYTES] = peer_id.as_bytes().try_into().ok()?;
3333 NodeId::from_bytes(&bytes).ok()
3334 })
3335 .collect()
3336}
3337
3338fn discovery_dial_slot_limit(
3339 connected_count: usize,
3340 in_flight_count: usize,
3341 max_connections: usize,
3342 connection_headroom: usize,
3343 max_concurrent_dials: usize,
3344) -> usize {
3345 let soft_cap = max_connections.saturating_sub(connection_headroom);
3346 let available_connection_slots = soft_cap.saturating_sub(connected_count);
3347 let available_dial_slots = max_concurrent_dials.saturating_sub(in_flight_count);
3348 available_connection_slots.min(available_dial_slots)
3349}
3350
3351fn has_wanted_services(record: &ZakuraNodeRecord, wanted_services: &[ZakuraServiceId]) -> bool {
3352 wanted_services
3353 .iter()
3354 .all(|wanted| record.body.services.iter().any(|service| service == wanted))
3355}
3356
3357fn validate_discovery_direct_addrs(
3366 record: &ZakuraNodeRecord,
3367 policy: DiscoveryDirectAddrPolicy,
3368) -> Result<(), DiscoveryBookError> {
3369 if record.body.direct_addrs.is_empty() {
3370 return Err(DiscoveryBookError::NoUsableDirectAddress);
3371 }
3372
3373 for addr in &record.body.direct_addrs {
3374 let is_valid = match policy {
3375 DiscoveryDirectAddrPolicy::UntrustedPeer => is_discovery_dialable_addr(addr),
3376 DiscoveryDirectAddrPolicy::StaticConfigured => {
3377 is_static_discovery_configured_addr_usable(addr)
3378 }
3379 };
3380 if !is_valid {
3381 return Err(DiscoveryBookError::NonDialableDirectAddress { addr: *addr });
3382 }
3383 }
3384
3385 Ok(())
3386}
3387
3388fn has_discovery_dialable_direct_addrs(record: &ZakuraNodeRecord) -> bool {
3389 !record.body.direct_addrs.is_empty()
3390 && record
3391 .body
3392 .direct_addrs
3393 .iter()
3394 .all(is_discovery_dialable_addr)
3395}
3396
3397fn has_discovery_usable_direct_addrs(entry: &ZakuraDiscoveryEntry) -> bool {
3398 !entry.record.body.direct_addrs.is_empty()
3399 && entry.record.body.direct_addrs.iter().all(|addr| {
3400 if entry.is_static {
3401 is_static_discovery_configured_addr_usable(addr)
3402 } else {
3403 is_discovery_dialable_addr(addr)
3404 }
3405 })
3406}
3407
3408fn entry_has_confirmed_dial_authority(entry: &ZakuraDiscoveryEntry) -> bool {
3409 entry.is_static || entry.source == Some(entry.record.body.node_id)
3410}
3411
3412fn is_discovery_dialable_addr(addr: &SocketAddr) -> bool {
3421 if addr.port() == 0 {
3422 return false;
3423 }
3424
3425 match addr.ip() {
3426 IpAddr::V4(ip) => {
3427 !ip.is_unspecified()
3428 && !ip.is_loopback()
3429 && !ip.is_multicast()
3430 && !ip.is_broadcast()
3431 && !ip.is_link_local()
3432 && !ip.is_private()
3433 && !is_ipv4_shared(&ip)
3434 }
3435 IpAddr::V6(ip) => {
3436 !ip.is_unspecified()
3437 && !ip.is_loopback()
3438 && !ip.is_multicast()
3439 && !is_ipv6_unicast_link_local(&ip)
3440 && !is_ipv6_unique_local(&ip)
3441 }
3442 }
3443}
3444
3445fn is_static_discovery_configured_addr_usable(addr: &SocketAddr) -> bool {
3446 if addr.port() == 0 {
3447 return false;
3448 }
3449
3450 match addr.ip() {
3451 IpAddr::V4(ip) => !ip.is_unspecified() && !ip.is_multicast() && !ip.is_broadcast(),
3452 IpAddr::V6(ip) => !ip.is_unspecified() && !ip.is_multicast(),
3453 }
3454}
3455
3456fn is_ipv6_unicast_link_local(ip: &Ipv6Addr) -> bool {
3457 (ip.segments()[0] & 0xffc0) == 0xfe80
3458}
3459
3460fn is_ipv4_shared(ip: &Ipv4Addr) -> bool {
3463 let octets = ip.octets();
3464 octets[0] == 100 && (octets[1] & 0b1100_0000) == 0b0100_0000
3465}
3466
3467fn is_ipv6_unique_local(ip: &Ipv6Addr) -> bool {
3469 (ip.segments()[0] & 0xfe00) == 0xfc00
3470}
3471
3472fn entry_is_expired(entry: &ZakuraDiscoveryEntry, now: u64) -> bool {
3474 entry.record.body.expires_at_unix_secs < now
3475}
3476
3477fn entry_in_dial_backoff(
3478 entry: &ZakuraDiscoveryEntry,
3479 now: u64,
3480 dial_backoff_base: Duration,
3481 dial_backoff_max: Duration,
3482) -> bool {
3483 entry_metadata_in_dial_backoff(
3484 entry.last_dial_attempt,
3485 entry.failure_count,
3486 now,
3487 dial_backoff_base,
3488 dial_backoff_max,
3489 )
3490}
3491
3492fn entry_metadata_in_dial_backoff(
3493 last_dial_attempt: Option<u64>,
3494 failure_count: u32,
3495 now: u64,
3496 dial_backoff_base: Duration,
3497 dial_backoff_max: Duration,
3498) -> bool {
3499 let Some(last_dial_attempt) = last_dial_attempt else {
3500 return false;
3501 };
3502 let backoff = dial_backoff_secs(
3503 failure_count,
3504 dial_backoff_base.as_secs(),
3505 dial_backoff_max.as_secs(),
3506 );
3507 now < last_dial_attempt.saturating_add(backoff)
3508}
3509
3510fn entry_in_short_lived_exchange_backoff(
3511 entry: &ZakuraDiscoveryEntry,
3512 now: u64,
3513 dial_backoff_base: Duration,
3514 dial_backoff_max: Duration,
3515) -> bool {
3516 entry_metadata_in_short_lived_exchange_backoff(
3517 entry.last_short_lived_exchange,
3518 now,
3519 dial_backoff_base,
3520 dial_backoff_max,
3521 )
3522}
3523
3524fn entry_metadata_in_short_lived_exchange_backoff(
3525 last_short_lived_exchange: Option<u64>,
3526 now: u64,
3527 dial_backoff_base: Duration,
3528 dial_backoff_max: Duration,
3529) -> bool {
3530 let Some(last_short_lived_exchange) = last_short_lived_exchange else {
3531 return false;
3532 };
3533 let backoff = dial_backoff_secs(1, dial_backoff_base.as_secs(), dial_backoff_max.as_secs());
3534 now < last_short_lived_exchange.saturating_add(backoff)
3535}
3536
3537fn dial_backoff_secs(failure_count: u32, base_secs: u64, max_secs: u64) -> u64 {
3538 if failure_count == 0 {
3539 return 0;
3540 }
3541
3542 let shift = failure_count.saturating_sub(1).min(10);
3543 base_secs.saturating_mul(1u64 << shift).min(max_secs)
3544}
3545
3546fn node_id_sort_key(node_id: &NodeId) -> [u8; NODE_ID_BYTES] {
3547 *node_id.as_bytes()
3548}
3549
3550fn validate_record_body_for_import(
3551 body: &ZakuraNodeRecordBody,
3552 context: &DiscoveryRecordValidationContext,
3553) -> Result<(), DiscoveryRecordError> {
3554 validate_record_body_bounds(body)?;
3555 if body.network_id != context.expected_network_id {
3556 return Err(DiscoveryRecordError::WrongNetwork);
3557 }
3558 if body.chain_id != context.expected_chain_id {
3559 return Err(DiscoveryRecordError::WrongChain);
3560 }
3561 if body.zakura_protocol_min > body.zakura_protocol_max
3562 || context.supported_protocol_min > context.supported_protocol_max
3563 || body.zakura_protocol_max < context.supported_protocol_min
3564 || context.supported_protocol_max < body.zakura_protocol_min
3565 {
3566 return Err(DiscoveryRecordError::IncompatibleProtocol);
3567 }
3568
3569 let skew = context.clock_skew_tolerance.as_secs();
3570 if body.expires_at_unix_secs.saturating_add(skew) < context.current_unix_secs {
3571 return Err(DiscoveryRecordError::Expired);
3572 }
3573
3574 let max_future = context
3575 .current_unix_secs
3576 .saturating_add(context.max_record_ttl.as_secs())
3577 .saturating_add(skew);
3578 if body.expires_at_unix_secs > max_future {
3579 return Err(DiscoveryRecordError::FarFutureExpiry);
3580 }
3581
3582 Ok(())
3583}
3584
3585fn validate_record_body_bounds(body: &ZakuraNodeRecordBody) -> Result<(), DiscoveryWireError> {
3586 if body.direct_addrs.len() > MAX_DIRECT_ADDRS_PER_RECORD {
3587 return Err(DiscoveryWireError::OversizedPayload {
3588 actual: body.direct_addrs.len(),
3589 max: MAX_DIRECT_ADDRS_PER_RECORD,
3590 });
3591 }
3592 if body.services.len() > MAX_SERVICES_PER_RECORD {
3593 return Err(DiscoveryWireError::OversizedPayload {
3594 actual: body.services.len(),
3595 max: MAX_SERVICES_PER_RECORD,
3596 });
3597 }
3598 for service in &body.services {
3599 validate_service_id(service.as_str().as_bytes())?;
3600 }
3601 if body.zakura_protocol_min > body.zakura_protocol_max {
3602 return Err(DiscoveryWireError::InvalidProtocolRange);
3603 }
3604 Ok(())
3605}
3606
3607fn validate_query_fields(
3608 limit: u16,
3609 wanted_services: &[ZakuraServiceId],
3610 exclude_node_ids: &[NodeId],
3611) -> Result<(), DiscoveryWireError> {
3612 if usize::from(limit) > MAX_DISCOVERY_RECORDS_PER_RESPONSE {
3613 return Err(DiscoveryWireError::OversizedPayload {
3614 actual: usize::from(limit),
3615 max: MAX_DISCOVERY_RECORDS_PER_RESPONSE,
3616 });
3617 }
3618 if wanted_services.len() > MAX_SERVICES_PER_RECORD {
3619 return Err(DiscoveryWireError::OversizedPayload {
3620 actual: wanted_services.len(),
3621 max: MAX_SERVICES_PER_RECORD,
3622 });
3623 }
3624 if exclude_node_ids.len() > MAX_DISCOVERY_EXCLUDED_NODE_IDS {
3625 return Err(DiscoveryWireError::OversizedPayload {
3626 actual: exclude_node_ids.len(),
3627 max: MAX_DISCOVERY_EXCLUDED_NODE_IDS,
3628 });
3629 }
3630 for service in wanted_services {
3631 validate_service_id(service.as_str().as_bytes())?;
3632 }
3633 Ok(())
3634}
3635
3636fn validate_get_services(query: &GetServices) -> Result<(), DiscoveryWireError> {
3637 validate_service_ids(&query.wanted_services, MAX_GET_SERVICES_WANTED)
3638}
3639
3640fn validate_services(services: &Services) -> Result<(), DiscoveryWireError> {
3641 if services.summaries.len() > MAX_SERVICE_SUMMARIES_PER_RESPONSE {
3642 return Err(DiscoveryWireError::OversizedPayload {
3643 actual: services.summaries.len(),
3644 max: MAX_SERVICE_SUMMARIES_PER_RESPONSE,
3645 });
3646 }
3647 for summary in &services.summaries {
3648 validate_summary_envelope(summary)?;
3649 }
3650 Ok(())
3651}
3652
3653fn validate_summary_envelope(envelope: &ServiceSummaryEnvelope) -> Result<(), DiscoveryWireError> {
3654 validate_service_id(envelope.service_id.as_str().as_bytes())?;
3655 if envelope.summary_bytes.len() > MAX_SERVICE_SUMMARY_BYTES {
3656 return Err(DiscoveryWireError::OversizedPayload {
3657 actual: envelope.summary_bytes.len(),
3658 max: MAX_SERVICE_SUMMARY_BYTES,
3659 });
3660 }
3661 validate_summary_service_binding(envelope.summary_tag, &envelope.service_id)?;
3662 validate_known_summary_payload(envelope.summary_tag, &envelope.summary_bytes)?;
3663 Ok(())
3664}
3665
3666fn validate_summary_service_binding(
3667 summary_tag: u16,
3668 service_id: &ZakuraServiceId,
3669) -> Result<(), DiscoveryWireError> {
3670 let expected = match summary_tag {
3671 SUMMARY_TAG_HEADER_SYNC => Some(ZakuraServiceId::header_sync()),
3672 SUMMARY_TAG_HEADER_SYNC_RETIRED => Some(ZakuraServiceId::header_sync_retired()),
3673 SUMMARY_TAG_DISCOVERY_V1 => Some(ZakuraServiceId::discovery()),
3674 SUMMARY_TAG_BLOCK_SYNC_V1 => Some(ZakuraServiceId::block_sync()),
3675 _ => None,
3676 };
3677
3678 if expected
3679 .as_ref()
3680 .is_some_and(|expected| expected != service_id)
3681 {
3682 return Err(DiscoveryWireError::MismatchedSummaryService {
3683 summary_tag,
3684 service_id: service_id.clone(),
3685 });
3686 }
3687
3688 Ok(())
3689}
3690
3691fn validate_known_summary_payload(
3692 summary_tag: u16,
3693 summary_bytes: &[u8],
3694) -> Result<(), DiscoveryWireError> {
3695 match summary_tag {
3696 SUMMARY_TAG_HEADER_SYNC | SUMMARY_TAG_HEADER_SYNC_RETIRED => {
3697 decode_header_sync_summary(summary_bytes)?;
3698 }
3699 SUMMARY_TAG_DISCOVERY_V1 => {
3700 decode_discovery_summary(summary_bytes)?;
3701 }
3702 SUMMARY_TAG_BLOCK_SYNC_V1 => {
3703 decode_block_sync_summary(summary_bytes)?;
3704 }
3705 _ => {}
3706 }
3707 Ok(())
3708}
3709
3710fn validate_service_id(bytes: &[u8]) -> Result<(), DiscoveryWireError> {
3711 if bytes.is_empty() {
3712 return Err(DiscoveryWireError::Empty("service id"));
3713 }
3714 if bytes.len() > MAX_ZAKURA_SERVICE_ID_BYTES {
3715 return Err(DiscoveryWireError::OversizedPayload {
3716 actual: bytes.len(),
3717 max: MAX_ZAKURA_SERVICE_ID_BYTES,
3718 });
3719 }
3720 if !bytes.is_ascii() {
3721 return Err(DiscoveryWireError::NonAsciiServiceId);
3722 }
3723 Ok(())
3724}
3725
3726fn validate_service_ids(
3727 services: &[ZakuraServiceId],
3728 max_count: usize,
3729) -> Result<(), DiscoveryWireError> {
3730 if services.len() > max_count {
3731 return Err(DiscoveryWireError::OversizedPayload {
3732 actual: services.len(),
3733 max: max_count,
3734 });
3735 }
3736 for service in services {
3737 validate_service_id(service.as_str().as_bytes())?;
3738 }
3739 Ok(())
3740}
3741
3742fn encode_record(
3743 record: &ZakuraNodeRecord,
3744 writer: &mut impl Write,
3745) -> Result<(), DiscoveryWireError> {
3746 validate_record_body_bounds(&record.body)?;
3747 let mut body_bytes = Vec::new();
3748 encode_record_body_to(&record.body, &mut body_bytes)?;
3749 if body_bytes.len() > MAX_NODE_RECORD_BODY_BYTES {
3750 return Err(DiscoveryWireError::OversizedPayload {
3751 actual: body_bytes.len(),
3752 max: MAX_NODE_RECORD_BODY_BYTES,
3753 });
3754 }
3755 writer.write_u32::<LittleEndian>(u32_from_usize(body_bytes.len(), "record body length")?)?;
3756 writer.write_all(&body_bytes)?;
3757 writer.write_all(&record.signature.to_bytes())?;
3758 Ok(())
3759}
3760
3761fn decode_record(reader: &mut impl Read) -> Result<ZakuraNodeRecord, DiscoveryWireError> {
3762 let body_len = usize_from_u32(reader.read_u32::<LittleEndian>()?, "record body length")?;
3763 if body_len > MAX_NODE_RECORD_BODY_BYTES {
3764 return Err(DiscoveryWireError::OversizedPayload {
3765 actual: body_len,
3766 max: MAX_NODE_RECORD_BODY_BYTES,
3767 });
3768 }
3769
3770 let body_bytes = read_exact_vec(reader, body_len)?;
3771 let body = decode_record_body(&body_bytes)?;
3772
3773 let mut signature_bytes = [0u8; SIGNATURE_BYTES];
3774 reader.read_exact(&mut signature_bytes)?;
3775 let signature = Signature::from(signature_bytes);
3776
3777 Ok(ZakuraNodeRecord { body, signature })
3778}
3779
3780fn encode_records_message(
3781 message_type: u8,
3782 records: &[ZakuraNodeRecord],
3783 max_count: usize,
3784 writer: &mut impl Write,
3785) -> Result<(), DiscoveryWireError> {
3786 if records.len() > max_count {
3787 return Err(DiscoveryWireError::OversizedPayload {
3788 actual: records.len(),
3789 max: max_count,
3790 });
3791 }
3792 writer.write_u8(message_type)?;
3793 writer.write_u16::<LittleEndian>(u16_from_usize(records.len(), "record count")?)?;
3794 for record in records {
3795 encode_record(record, writer)?;
3796 }
3797 Ok(())
3798}
3799
3800fn decode_record_list(
3801 reader: &mut impl Read,
3802 max_count: usize,
3803) -> Result<Vec<ZakuraNodeRecord>, DiscoveryWireError> {
3804 let count = usize::from(reader.read_u16::<LittleEndian>()?);
3805 if count > max_count {
3806 return Err(DiscoveryWireError::OversizedPayload {
3807 actual: count,
3808 max: max_count,
3809 });
3810 }
3811 let mut records = Vec::with_capacity(count);
3812 for _ in 0..count {
3813 records.push(decode_record(reader)?);
3814 }
3815 Ok(records)
3816}
3817
3818fn encode_query_fields(
3819 limit: u16,
3820 wanted_services: &[ZakuraServiceId],
3821 exclude_node_ids: &[NodeId],
3822 writer: &mut impl Write,
3823) -> Result<(), DiscoveryWireError> {
3824 writer.write_u16::<LittleEndian>(limit)?;
3825 encode_service_ids(wanted_services, writer)?;
3826 encode_node_ids(exclude_node_ids, writer)?;
3827 Ok(())
3828}
3829
3830fn decode_query_fields(
3831 reader: &mut impl Read,
3832) -> Result<(u16, Vec<ZakuraServiceId>, Vec<NodeId>), DiscoveryWireError> {
3833 let limit = reader.read_u16::<LittleEndian>()?;
3834 let wanted_services = decode_service_ids(reader, MAX_SERVICES_PER_RECORD)?;
3835 let exclude_node_ids = decode_node_ids(reader, MAX_DISCOVERY_EXCLUDED_NODE_IDS)?;
3836 validate_query_fields(limit, &wanted_services, &exclude_node_ids)?;
3837 Ok((limit, wanted_services, exclude_node_ids))
3838}
3839
3840fn decode_get_services(reader: &mut impl Read) -> Result<GetServices, DiscoveryWireError> {
3841 let wanted_services = decode_service_ids(reader, MAX_GET_SERVICES_WANTED)?;
3842 Ok(GetServices { wanted_services })
3843}
3844
3845fn encode_services_message(
3846 services: &Services,
3847 writer: &mut impl Write,
3848) -> Result<(), DiscoveryWireError> {
3849 writer.write_all(services.node_id.as_bytes())?;
3850 writer.write_u64::<LittleEndian>(services.expires_at_unix_secs)?;
3851 writer.write_u16::<LittleEndian>(u16_from_usize(
3852 services.summaries.len(),
3853 "service summary count",
3854 )?)?;
3855 for summary in &services.summaries {
3856 encode_summary_envelope(summary, writer)?;
3857 }
3858 Ok(())
3859}
3860
3861fn decode_services_message(reader: &mut Cursor<&[u8]>) -> Result<Services, DiscoveryWireError> {
3862 let node_id = decode_node_id(reader)?;
3863 let expires_at_unix_secs = reader.read_u64::<LittleEndian>()?;
3864 let count = usize::from(reader.read_u16::<LittleEndian>()?);
3865 if count > MAX_SERVICE_SUMMARIES_PER_RESPONSE {
3866 return Err(DiscoveryWireError::OversizedPayload {
3867 actual: count,
3868 max: MAX_SERVICE_SUMMARIES_PER_RESPONSE,
3869 });
3870 }
3871
3872 let mut summaries = Vec::with_capacity(count);
3873 for _ in 0..count {
3874 summaries.push(decode_summary_envelope(reader)?);
3875 }
3876
3877 let services = Services {
3878 node_id,
3879 expires_at_unix_secs,
3880 summaries,
3881 };
3882 validate_services(&services)?;
3883 Ok(services)
3884}
3885
3886fn encode_summary_envelope(
3887 envelope: &ServiceSummaryEnvelope,
3888 writer: &mut impl Write,
3889) -> Result<(), DiscoveryWireError> {
3890 validate_summary_envelope(envelope)?;
3891 encode_service_id(&envelope.service_id, writer)?;
3892 writer.write_u16::<LittleEndian>(envelope.summary_tag)?;
3893 writer.write_u16::<LittleEndian>(u16_from_usize(
3894 envelope.summary_bytes.len(),
3895 "service summary length",
3896 )?)?;
3897 writer.write_all(&envelope.summary_bytes)?;
3898 Ok(())
3899}
3900
3901fn decode_summary_envelope(
3902 reader: &mut Cursor<&[u8]>,
3903) -> Result<ServiceSummaryEnvelope, DiscoveryWireError> {
3904 let service_id = decode_service_id(reader)?;
3905 let summary_tag = reader.read_u16::<LittleEndian>()?;
3906 let summary_len = usize::from(reader.read_u16::<LittleEndian>()?);
3907 if summary_len > MAX_SERVICE_SUMMARY_BYTES {
3908 return Err(DiscoveryWireError::OversizedPayload {
3909 actual: summary_len,
3910 max: MAX_SERVICE_SUMMARY_BYTES,
3911 });
3912 }
3913 let remaining = cursor_remaining(reader)?;
3914 if summary_len > remaining {
3915 return Err(DiscoveryWireError::TruncatedPayload {
3916 declared: summary_len,
3917 remaining,
3918 });
3919 }
3920 let summary_bytes = read_exact_vec(reader, summary_len)?;
3921 validate_known_summary_payload(summary_tag, &summary_bytes)?;
3922 Ok(ServiceSummaryEnvelope {
3923 service_id,
3924 summary_tag,
3925 summary_bytes,
3926 })
3927}
3928
3929fn encode_record_body_to(
3930 body: &ZakuraNodeRecordBody,
3931 writer: &mut impl Write,
3932) -> Result<(), DiscoveryWireError> {
3933 writer.write_all(body.node_id.as_bytes())?;
3934 encode_socket_addrs(&body.direct_addrs, writer)?;
3935 encode_service_ids(&body.services, writer)?;
3936 writer.write_u16::<LittleEndian>(body.zakura_protocol_min)?;
3937 writer.write_u16::<LittleEndian>(body.zakura_protocol_max)?;
3938 writer.write_u32::<LittleEndian>(body.network_id.code())?;
3939 writer.write_all(&body.chain_id)?;
3940 writer.write_u64::<LittleEndian>(body.sequence)?;
3941 writer.write_u64::<LittleEndian>(body.expires_at_unix_secs)?;
3942 Ok(())
3943}
3944
3945fn decode_record_body(bytes: &[u8]) -> Result<ZakuraNodeRecordBody, DiscoveryWireError> {
3946 if bytes.len() > MAX_NODE_RECORD_BODY_BYTES {
3947 return Err(DiscoveryWireError::OversizedPayload {
3948 actual: bytes.len(),
3949 max: MAX_NODE_RECORD_BODY_BYTES,
3950 });
3951 }
3952 let mut reader = Cursor::new(bytes);
3953 let mut node_id_bytes = [0u8; NODE_ID_BYTES];
3954 reader.read_exact(&mut node_id_bytes)?;
3955 let node_id =
3956 NodeId::from_bytes(&node_id_bytes).map_err(|_| DiscoveryWireError::InvalidNodeId)?;
3957 let direct_addrs = decode_socket_addrs(&mut reader)?;
3958 let services = decode_service_ids(&mut reader, MAX_SERVICES_PER_RECORD)?;
3959 let zakura_protocol_min = reader.read_u16::<LittleEndian>()?;
3960 let zakura_protocol_max = reader.read_u16::<LittleEndian>()?;
3961 let network_id = network_id_from_code(reader.read_u32::<LittleEndian>()?)?;
3962 let mut chain_id = [0u8; 32];
3963 reader.read_exact(&mut chain_id)?;
3964 let sequence = reader.read_u64::<LittleEndian>()?;
3965 let expires_at_unix_secs = reader.read_u64::<LittleEndian>()?;
3966 reject_trailing(bytes, &reader)?;
3967
3968 let body = ZakuraNodeRecordBody {
3969 node_id,
3970 direct_addrs,
3971 services,
3972 zakura_protocol_min,
3973 zakura_protocol_max,
3974 network_id,
3975 chain_id,
3976 sequence,
3977 expires_at_unix_secs,
3978 };
3979 validate_record_body_bounds(&body)?;
3980 Ok(body)
3981}
3982
3983fn encode_socket_addrs(
3984 addrs: &[SocketAddr],
3985 writer: &mut impl Write,
3986) -> Result<(), DiscoveryWireError> {
3987 if addrs.len() > MAX_DIRECT_ADDRS_PER_RECORD {
3988 return Err(DiscoveryWireError::OversizedPayload {
3989 actual: addrs.len(),
3990 max: MAX_DIRECT_ADDRS_PER_RECORD,
3991 });
3992 }
3993 writer.write_u16::<LittleEndian>(u16_from_usize(addrs.len(), "direct address count")?)?;
3994 for addr in addrs {
3995 match addr {
3996 SocketAddr::V4(addr) => {
3997 writer.write_u8(SOCKET_ADDR_V4)?;
3998 writer.write_all(&addr.ip().octets())?;
3999 writer.write_u16::<LittleEndian>(addr.port())?;
4000 }
4001 SocketAddr::V6(addr) => {
4002 writer.write_u8(SOCKET_ADDR_V6)?;
4003 writer.write_all(&addr.ip().octets())?;
4004 writer.write_u16::<LittleEndian>(addr.port())?;
4005 writer.write_u32::<LittleEndian>(addr.flowinfo())?;
4006 writer.write_u32::<LittleEndian>(addr.scope_id())?;
4007 }
4008 }
4009 }
4010 Ok(())
4011}
4012
4013fn decode_socket_addrs(reader: &mut impl Read) -> Result<Vec<SocketAddr>, DiscoveryWireError> {
4014 let count = usize::from(reader.read_u16::<LittleEndian>()?);
4015 if count > MAX_DIRECT_ADDRS_PER_RECORD {
4016 return Err(DiscoveryWireError::OversizedPayload {
4017 actual: count,
4018 max: MAX_DIRECT_ADDRS_PER_RECORD,
4019 });
4020 }
4021 let mut addrs = Vec::with_capacity(count);
4022 for _ in 0..count {
4023 let addr = match reader.read_u8()? {
4024 SOCKET_ADDR_V4 => {
4025 let mut octets = [0u8; 4];
4026 reader.read_exact(&mut octets)?;
4027 let port = reader.read_u16::<LittleEndian>()?;
4028 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(octets), port))
4029 }
4030 SOCKET_ADDR_V6 => {
4031 let mut octets = [0u8; 16];
4032 reader.read_exact(&mut octets)?;
4033 let port = reader.read_u16::<LittleEndian>()?;
4034 let flowinfo = reader.read_u32::<LittleEndian>()?;
4035 let scope_id = reader.read_u32::<LittleEndian>()?;
4036 SocketAddr::V6(SocketAddrV6::new(
4037 Ipv6Addr::from(octets),
4038 port,
4039 flowinfo,
4040 scope_id,
4041 ))
4042 }
4043 family => return Err(DiscoveryWireError::InvalidAddressFamily(family)),
4044 };
4045 addrs.push(addr);
4046 }
4047 Ok(addrs)
4048}
4049
4050fn encode_service_ids(
4051 services: &[ZakuraServiceId],
4052 writer: &mut impl Write,
4053) -> Result<(), DiscoveryWireError> {
4054 encode_service_ids_capped(services, MAX_SERVICES_PER_RECORD, writer)
4055}
4056
4057fn encode_service_ids_capped(
4058 services: &[ZakuraServiceId],
4059 max_count: usize,
4060 writer: &mut impl Write,
4061) -> Result<(), DiscoveryWireError> {
4062 if services.len() > max_count {
4063 return Err(DiscoveryWireError::OversizedPayload {
4064 actual: services.len(),
4065 max: max_count,
4066 });
4067 }
4068 writer.write_u16::<LittleEndian>(u16_from_usize(services.len(), "service count")?)?;
4069 for service in services {
4070 encode_service_id(service, writer)?;
4071 }
4072 Ok(())
4073}
4074
4075fn encode_service_id(
4076 service: &ZakuraServiceId,
4077 writer: &mut impl Write,
4078) -> Result<(), DiscoveryWireError> {
4079 let bytes = service.as_str().as_bytes();
4080 validate_service_id(bytes)?;
4081 writer.write_u16::<LittleEndian>(u16_from_usize(bytes.len(), "service id length")?)?;
4082 writer.write_all(bytes)?;
4083 Ok(())
4084}
4085
4086fn decode_service_ids(
4087 reader: &mut impl Read,
4088 max_count: usize,
4089) -> Result<Vec<ZakuraServiceId>, DiscoveryWireError> {
4090 let count = usize::from(reader.read_u16::<LittleEndian>()?);
4091 if count > max_count {
4092 return Err(DiscoveryWireError::OversizedPayload {
4093 actual: count,
4094 max: max_count,
4095 });
4096 }
4097 let mut services = Vec::with_capacity(count);
4098 for _ in 0..count {
4099 services.push(decode_service_id(reader)?);
4100 }
4101 Ok(services)
4102}
4103
4104fn decode_service_id(reader: &mut impl Read) -> Result<ZakuraServiceId, DiscoveryWireError> {
4105 let len = usize::from(reader.read_u16::<LittleEndian>()?);
4106 if len > MAX_ZAKURA_SERVICE_ID_BYTES {
4107 return Err(DiscoveryWireError::OversizedPayload {
4108 actual: len,
4109 max: MAX_ZAKURA_SERVICE_ID_BYTES,
4110 });
4111 }
4112 let bytes = read_exact_vec(reader, len)?;
4113 validate_service_id(&bytes)?;
4114 let service = String::from_utf8(bytes).map_err(|_| DiscoveryWireError::NonAsciiServiceId)?;
4115 Ok(ZakuraServiceId(service))
4116}
4117
4118fn encode_node_ids(node_ids: &[NodeId], writer: &mut impl Write) -> Result<(), DiscoveryWireError> {
4119 if node_ids.len() > MAX_DISCOVERY_EXCLUDED_NODE_IDS {
4120 return Err(DiscoveryWireError::OversizedPayload {
4121 actual: node_ids.len(),
4122 max: MAX_DISCOVERY_EXCLUDED_NODE_IDS,
4123 });
4124 }
4125 writer.write_u16::<LittleEndian>(u16_from_usize(node_ids.len(), "node id count")?)?;
4126 for node_id in node_ids {
4127 writer.write_all(node_id.as_bytes())?;
4128 }
4129 Ok(())
4130}
4131
4132fn decode_node_ids(
4133 reader: &mut impl Read,
4134 max_count: usize,
4135) -> Result<Vec<NodeId>, DiscoveryWireError> {
4136 let count = usize::from(reader.read_u16::<LittleEndian>()?);
4137 if count > max_count {
4138 return Err(DiscoveryWireError::OversizedPayload {
4139 actual: count,
4140 max: max_count,
4141 });
4142 }
4143 let mut node_ids = Vec::with_capacity(count);
4144 for _ in 0..count {
4145 node_ids.push(decode_node_id(reader)?);
4146 }
4147 Ok(node_ids)
4148}
4149
4150fn decode_node_id(reader: &mut impl Read) -> Result<NodeId, DiscoveryWireError> {
4151 let mut bytes = [0u8; NODE_ID_BYTES];
4152 reader.read_exact(&mut bytes)?;
4153 NodeId::from_bytes(&bytes).map_err(|_| DiscoveryWireError::InvalidNodeId)
4154}
4155
4156fn network_id_from_code(value: u32) -> Result<ZakuraNetworkId, DiscoveryWireError> {
4157 match value {
4158 1 => Ok(ZakuraNetworkId::Mainnet),
4159 2 => Ok(ZakuraNetworkId::Testnet),
4160 3 => Ok(ZakuraNetworkId::Regtest),
4161 4 => Ok(ZakuraNetworkId::Configured),
4162 _ => Err(DiscoveryWireError::InvalidNetworkId(value)),
4163 }
4164}
4165
4166fn encode_header_sync_summary(
4167 summary: &HeaderSyncServiceSummary,
4168 writer: &mut impl Write,
4169) -> Result<(), DiscoveryWireError> {
4170 write_height(writer, summary.best_height)?;
4171 writer.write_all(&summary.best_hash.0)?;
4172 write_optional_height(writer, summary.finalized_height)?;
4173 write_bool(writer, summary.serving_headers)?;
4174 writer.write_u16::<LittleEndian>(summary.inbound_slots_free)?;
4175 writer.write_u16::<LittleEndian>(summary.inbound_slots_max)?;
4176 writer.write_u16::<LittleEndian>(summary.outbound_slots_free)?;
4177 writer.write_u16::<LittleEndian>(summary.outbound_slots_max)?;
4178 Ok(())
4179}
4180
4181fn decode_header_sync_summary(
4182 bytes: &[u8],
4183) -> Result<HeaderSyncServiceSummary, DiscoveryWireError> {
4184 if bytes.len() > MAX_SERVICE_SUMMARY_BYTES {
4185 return Err(DiscoveryWireError::OversizedPayload {
4186 actual: bytes.len(),
4187 max: MAX_SERVICE_SUMMARY_BYTES,
4188 });
4189 }
4190 let mut reader = Cursor::new(bytes);
4191 let best_height = read_height(&mut reader)?;
4192 let mut best_hash = [0u8; 32];
4193 reader.read_exact(&mut best_hash)?;
4194 let finalized_height = read_optional_height(&mut reader)?;
4195 let serving_headers = read_bool(&mut reader, "header sync serving_headers")?;
4196 let inbound_slots_free = reader.read_u16::<LittleEndian>()?;
4197 let inbound_slots_max = reader.read_u16::<LittleEndian>()?;
4198 let outbound_slots_free = reader.read_u16::<LittleEndian>()?;
4199 let outbound_slots_max = reader.read_u16::<LittleEndian>()?;
4200 reject_trailing(bytes, &reader)?;
4201 Ok(HeaderSyncServiceSummary {
4202 best_height,
4203 best_hash: block::Hash(best_hash),
4204 finalized_height,
4205 serving_headers,
4206 inbound_slots_free,
4207 inbound_slots_max,
4208 outbound_slots_free,
4209 outbound_slots_max,
4210 })
4211}
4212
4213fn encode_discovery_summary(
4214 summary: &DiscoveryServiceSummary,
4215 writer: &mut impl Write,
4216) -> Result<(), DiscoveryWireError> {
4217 writer.write_u16::<LittleEndian>(summary.peer_exchange_slots_free)?;
4218 writer.write_u16::<LittleEndian>(summary.max_records_per_response)?;
4219 write_bool(writer, summary.expected_disconnect_after_exchange)?;
4220 Ok(())
4221}
4222
4223fn decode_discovery_summary(bytes: &[u8]) -> Result<DiscoveryServiceSummary, DiscoveryWireError> {
4224 if bytes.len() > MAX_SERVICE_SUMMARY_BYTES {
4225 return Err(DiscoveryWireError::OversizedPayload {
4226 actual: bytes.len(),
4227 max: MAX_SERVICE_SUMMARY_BYTES,
4228 });
4229 }
4230 let mut reader = Cursor::new(bytes);
4231 let peer_exchange_slots_free = reader.read_u16::<LittleEndian>()?;
4232 let max_records_per_response = reader.read_u16::<LittleEndian>()?;
4233 let expected_disconnect_after_exchange =
4234 read_bool(&mut reader, "discovery expected_disconnect_after_exchange")?;
4235 reject_trailing(bytes, &reader)?;
4236 Ok(DiscoveryServiceSummary {
4237 peer_exchange_slots_free,
4238 max_records_per_response,
4239 expected_disconnect_after_exchange,
4240 })
4241}
4242
4243fn encode_block_sync_summary(
4244 summary: &BlockSyncServiceSummary,
4245 writer: &mut impl Write,
4246) -> Result<(), DiscoveryWireError> {
4247 validate_block_sync_summary(summary)?;
4248 write_height(writer, summary.servable_low)?;
4249 write_height(writer, summary.servable_high)?;
4250 writer.write_all(&summary.tip_hash.0)?;
4251 writer.write_u16::<LittleEndian>(summary.free_slots)?;
4252 writer.write_u32::<LittleEndian>(summary.max_blocks_per_response)?;
4253 writer.write_u32::<LittleEndian>(summary.max_response_bytes)?;
4254 Ok(())
4255}
4256
4257fn decode_block_sync_summary(bytes: &[u8]) -> Result<BlockSyncServiceSummary, DiscoveryWireError> {
4258 if bytes.len() > MAX_SERVICE_SUMMARY_BYTES {
4259 return Err(DiscoveryWireError::OversizedPayload {
4260 actual: bytes.len(),
4261 max: MAX_SERVICE_SUMMARY_BYTES,
4262 });
4263 }
4264 let mut reader = Cursor::new(bytes);
4265 let servable_low = read_height(&mut reader)?;
4266 let servable_high = read_height(&mut reader)?;
4267 let mut tip_hash = [0u8; 32];
4268 reader.read_exact(&mut tip_hash)?;
4269 let free_slots = reader.read_u16::<LittleEndian>()?;
4270 let max_blocks_per_response = reader.read_u32::<LittleEndian>()?;
4271 let max_response_bytes = reader.read_u32::<LittleEndian>()?;
4272 reject_trailing(bytes, &reader)?;
4273 let summary = BlockSyncServiceSummary {
4274 servable_low,
4275 servable_high,
4276 tip_hash: block::Hash(tip_hash),
4277 free_slots,
4278 max_blocks_per_response,
4279 max_response_bytes,
4280 };
4281 validate_block_sync_summary(&summary)?;
4282 Ok(summary)
4283}
4284
4285fn validate_block_sync_summary(
4286 summary: &BlockSyncServiceSummary,
4287) -> Result<(), DiscoveryWireError> {
4288 if summary.servable_low > summary.servable_high {
4289 return Err(DiscoveryWireError::InvalidProtocolRange);
4290 }
4291 if summary.max_blocks_per_response == 0
4292 || summary.max_blocks_per_response > MAX_BS_BLOCKS_PER_REQUEST
4293 {
4294 return Err(DiscoveryWireError::OversizedPayload {
4295 actual: usize::try_from(summary.max_blocks_per_response).unwrap_or(usize::MAX),
4296 max: usize::try_from(MAX_BS_BLOCKS_PER_REQUEST).unwrap_or(usize::MAX),
4297 });
4298 }
4299 if summary.max_response_bytes == 0 || summary.max_response_bytes > MAX_BS_RESPONSE_BYTES {
4300 return Err(DiscoveryWireError::OversizedPayload {
4301 actual: usize::try_from(summary.max_response_bytes).unwrap_or(usize::MAX),
4302 max: usize::try_from(MAX_BS_RESPONSE_BYTES).unwrap_or(usize::MAX),
4303 });
4304 }
4305 Ok(())
4306}
4307
4308fn write_height(writer: &mut impl Write, height: block::Height) -> Result<(), DiscoveryWireError> {
4309 writer.write_u32::<LittleEndian>(height.0)?;
4310 Ok(())
4311}
4312
4313fn read_height(reader: &mut impl Read) -> Result<block::Height, DiscoveryWireError> {
4314 let height = reader.read_u32::<LittleEndian>()?;
4315 block::Height::try_from(height).map_err(|_| DiscoveryWireError::InvalidHeight(height))
4316}
4317
4318fn write_optional_height(
4319 writer: &mut impl Write,
4320 height: Option<block::Height>,
4321) -> Result<(), DiscoveryWireError> {
4322 match height {
4323 Some(height) => {
4324 writer.write_u8(1)?;
4325 write_height(writer, height)?;
4326 }
4327 None => writer.write_u8(0)?,
4328 }
4329 Ok(())
4330}
4331
4332fn read_optional_height(
4333 reader: &mut impl Read,
4334) -> Result<Option<block::Height>, DiscoveryWireError> {
4335 match reader.read_u8()? {
4336 0 => Ok(None),
4337 1 => read_height(reader).map(Some),
4338 value => Err(DiscoveryWireError::InvalidBoolean {
4339 field: "optional height present",
4340 value,
4341 }),
4342 }
4343}
4344
4345fn write_bool(writer: &mut impl Write, value: bool) -> Result<(), DiscoveryWireError> {
4346 writer.write_u8(u8::from(value))?;
4347 Ok(())
4348}
4349
4350fn read_bool(reader: &mut impl Read, field: &'static str) -> Result<bool, DiscoveryWireError> {
4351 match reader.read_u8()? {
4352 0 => Ok(false),
4353 1 => Ok(true),
4354 value => Err(DiscoveryWireError::InvalidBoolean { field, value }),
4355 }
4356}
4357
4358fn saturating_u16(value: usize) -> u16 {
4359 u16::try_from(value).unwrap_or(u16::MAX)
4360}
4361
4362fn cursor_remaining(reader: &Cursor<&[u8]>) -> Result<usize, DiscoveryWireError> {
4363 let position = usize::try_from(reader.position())
4364 .map_err(|_| DiscoveryWireError::NumericOverflow("cursor position"))?;
4365 Ok(reader.get_ref().len().saturating_sub(position))
4366}
4367
4368fn read_exact_vec(reader: &mut impl Read, len: usize) -> Result<Vec<u8>, DiscoveryWireError> {
4369 let mut bytes = vec![0; len];
4370 reader.read_exact(&mut bytes)?;
4371 Ok(bytes)
4372}
4373
4374fn reject_trailing(bytes: &[u8], reader: &Cursor<&[u8]>) -> Result<(), DiscoveryWireError> {
4375 let consumed = usize::try_from(reader.position())
4376 .map_err(|_| DiscoveryWireError::NumericOverflow("cursor position"))?;
4377 if consumed != bytes.len() {
4378 return Err(DiscoveryWireError::TrailingBytes);
4379 }
4380 Ok(())
4381}
4382
4383fn usize_from_u32(value: u32, field: &'static str) -> Result<usize, DiscoveryWireError> {
4384 usize::try_from(value).map_err(|_| DiscoveryWireError::NumericOverflow(field))
4385}
4386
4387fn u32_from_usize(value: usize, field: &'static str) -> Result<u32, DiscoveryWireError> {
4388 u32::try_from(value).map_err(|_| DiscoveryWireError::NumericOverflow(field))
4389}
4390
4391fn u16_from_usize(value: usize, field: &'static str) -> Result<u16, DiscoveryWireError> {
4392 u16::try_from(value).map_err(|_| DiscoveryWireError::NumericOverflow(field))
4393}
4394
4395#[cfg(test)]
4396mod tests {
4397 use std::{net::IpAddr, time::Duration};
4398
4399 use iroh::SecretKey;
4400 use rand::{rngs::OsRng, rngs::StdRng, SeedableRng};
4401
4402 use super::*;
4403
4404 const NOW: u64 = 1_700_000_000;
4405 const CHAIN_ID: [u8; 32] = [7; 32];
4406
4407 fn secret_key() -> SecretKey {
4408 SecretKey::generate(OsRng)
4409 }
4410
4411 fn service(index: usize) -> ZakuraServiceId {
4412 ZakuraServiceId::new(format!("zakura.test.{index}.v1")).expect("test service id is valid")
4413 }
4414
4415 fn context() -> DiscoveryRecordValidationContext {
4416 DiscoveryRecordValidationContext {
4417 expected_network_id: ZakuraNetworkId::Regtest,
4418 expected_chain_id: CHAIN_ID,
4419 current_unix_secs: NOW,
4420 supported_protocol_min: 1,
4421 supported_protocol_max: 1,
4422 max_record_ttl: Duration::from_secs(24 * 60 * 60),
4423 clock_skew_tolerance: Duration::from_secs(300),
4424 }
4425 }
4426
4427 fn body(secret_key: &SecretKey) -> ZakuraNodeRecordBody {
4428 ZakuraNodeRecordBody {
4429 node_id: secret_key.public(),
4430 direct_addrs: vec![
4431 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10)), 8233),
4432 SocketAddr::new(
4433 IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 10)),
4434 18233,
4435 ),
4436 ],
4437 services: vec![
4438 ZakuraServiceId::discovery(),
4439 ZakuraServiceId::legacy_gossip(),
4440 ZakuraServiceId::legacy_requests(),
4441 ],
4442 zakura_protocol_min: 1,
4443 zakura_protocol_max: 1,
4444 network_id: ZakuraNetworkId::Regtest,
4445 chain_id: CHAIN_ID,
4446 sequence: 42,
4447 expires_at_unix_secs: NOW + 60,
4448 }
4449 }
4450
4451 fn signed_record() -> ZakuraNodeRecord {
4452 let secret_key = secret_key();
4453 ZakuraNodeRecord::sign(body(&secret_key), &secret_key).expect("test record signs")
4454 }
4455
4456 fn sign_record_with_prefix(
4457 body: ZakuraNodeRecordBody,
4458 secret_key: &SecretKey,
4459 domain: &[u8],
4460 record_format_version: u16,
4461 ) -> ZakuraNodeRecord {
4462 let mut bytes = Vec::new();
4463 bytes.write_all(domain).expect("test domain writes");
4464 bytes
4465 .write_u16::<LittleEndian>(record_format_version)
4466 .expect("test record format version writes");
4467 encode_record_body_to(&body, &mut bytes).expect("test record body encodes");
4468
4469 let signing_key = SigningKey::from(secret_key.to_bytes());
4470 let signature = signing_key.sign(&bytes);
4471 ZakuraNodeRecord { body, signature }
4472 }
4473
4474 fn signed_record_with(
4475 sequence: u64,
4476 service: ZakuraServiceId,
4477 addr: SocketAddr,
4478 ) -> ZakuraNodeRecord {
4479 signed_record_with_addrs(sequence, service, vec![addr])
4480 }
4481
4482 fn signed_record_with_addrs(
4483 sequence: u64,
4484 service: ZakuraServiceId,
4485 addrs: Vec<SocketAddr>,
4486 ) -> ZakuraNodeRecord {
4487 let secret_key = secret_key();
4488 let mut body = body(&secret_key);
4489 body.sequence = sequence;
4490 body.direct_addrs = addrs;
4491 body.services = vec![service];
4492 ZakuraNodeRecord::sign(body, &secret_key).expect("test record signs")
4493 }
4494
4495 fn record_with_secret(
4496 secret_key: &SecretKey,
4497 sequence: u64,
4498 service: ZakuraServiceId,
4499 ) -> ZakuraNodeRecord {
4500 let mut body = body(secret_key);
4501 body.sequence = sequence;
4502 body.services = vec![service];
4503 ZakuraNodeRecord::sign(body, secret_key).expect("test record signs")
4504 }
4505
4506 fn test_addr(index: u8) -> SocketAddr {
4507 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 0, 2, index)), 8233)
4508 }
4509
4510 fn candidate_for(record: &ZakuraNodeRecord, is_static: bool) -> ZakuraDiscoveryDialCandidate {
4511 ZakuraDiscoveryDialCandidate {
4512 node_id: record.body.node_id,
4513 direct_addrs: record.body.direct_addrs.clone(),
4514 is_static,
4515 }
4516 }
4517
4518 fn import_confirmed_record(
4519 book: &mut ZakuraDiscoveryBook,
4520 record: ZakuraNodeRecord,
4521 ) -> Result<ImportOutcome, DiscoveryBookError> {
4522 let source = record.body.node_id;
4523 book.import_record(record, Some(source), NOW, &context())
4524 }
4525
4526 fn small_book(max_records: usize) -> ZakuraDiscoveryBook {
4527 ZakuraDiscoveryBook::new(ZakuraDiscoveryBookLimits {
4528 max_records,
4529 ..ZakuraDiscoveryBookLimits::default()
4530 })
4531 }
4532
4533 fn runtime_record_with(
4534 sequence: u64,
4535 service: ZakuraServiceId,
4536 addr: SocketAddr,
4537 ) -> ZakuraNodeRecord {
4538 let secret_key = secret_key();
4539 runtime_record_with_secret(&secret_key, sequence, service, addr)
4540 }
4541
4542 fn runtime_record_with_services(
4543 sequence: u64,
4544 services: Vec<ZakuraServiceId>,
4545 addr: SocketAddr,
4546 ) -> ZakuraNodeRecord {
4547 let secret_key = secret_key();
4548 let mut body = body(&secret_key);
4549 body.sequence = sequence;
4550 body.direct_addrs = vec![addr];
4551 body.services = services;
4552 body.expires_at_unix_secs = current_unix_secs() + DEFAULT_DISCOVERY_RECORD_TTL.as_secs();
4553 ZakuraNodeRecord::sign(body, &secret_key).expect("runtime test record signs")
4554 }
4555
4556 fn runtime_record_with_secret(
4557 secret_key: &SecretKey,
4558 sequence: u64,
4559 service: ZakuraServiceId,
4560 addr: SocketAddr,
4561 ) -> ZakuraNodeRecord {
4562 runtime_record_with_secret_and_addrs(secret_key, sequence, service, vec![addr])
4563 }
4564
4565 fn runtime_record_with_secret_and_addrs(
4566 secret_key: &SecretKey,
4567 sequence: u64,
4568 service: ZakuraServiceId,
4569 addrs: Vec<SocketAddr>,
4570 ) -> ZakuraNodeRecord {
4571 let mut body = body(secret_key);
4572 body.sequence = sequence;
4573 body.direct_addrs = addrs;
4574 body.services = vec![service];
4575 body.expires_at_unix_secs = current_unix_secs() + DEFAULT_DISCOVERY_RECORD_TTL.as_secs();
4576 ZakuraNodeRecord::sign(body, secret_key).expect("runtime test record signs")
4577 }
4578
4579 fn local_config_with(
4580 secret_key: SecretKey,
4581 direct_addrs: Vec<SocketAddr>,
4582 services: Vec<ZakuraServiceId>,
4583 ) -> ZakuraDiscoveryLocalConfig {
4584 ZakuraDiscoveryLocalConfig {
4585 secret_key,
4586 direct_addrs,
4587 services,
4588 zakura_protocol_min: 1,
4589 zakura_protocol_max: 1,
4590 network_id: ZakuraNetworkId::Regtest,
4591 chain_id: CHAIN_ID,
4592 last_authored_sequence: None,
4593 }
4594 }
4595
4596 fn discovery_handle_at(
4597 local_config: ZakuraDiscoveryLocalConfig,
4598 config: ZakuraDiscoveryConfig,
4599 connected: watch::Receiver<Vec<ZakuraPeerId>>,
4600 now: u64,
4601 ) -> ZakuraDiscoveryHandle {
4602 ZakuraDiscoveryHandle::new_at(local_config, config, connected, now, now)
4603 .expect("test discovery state constructs")
4604 }
4605
4606 fn discovery_handle_at_with_sequence(
4607 local_config: ZakuraDiscoveryLocalConfig,
4608 config: ZakuraDiscoveryConfig,
4609 connected: watch::Receiver<Vec<ZakuraPeerId>>,
4610 now: u64,
4611 wall_clock_sequence: u64,
4612 ) -> ZakuraDiscoveryHandle {
4613 ZakuraDiscoveryHandle::new_at(local_config, config, connected, now, wall_clock_sequence)
4614 .expect("test discovery state constructs")
4615 }
4616
4617 fn discovery_handle_with_connected(
4618 connected: watch::Receiver<Vec<ZakuraPeerId>>,
4619 ) -> ZakuraDiscoveryHandle {
4620 discovery_handle_at(
4621 local_config_with(
4622 secret_key(),
4623 vec![test_addr(200)],
4624 vec![ZakuraServiceId::discovery()],
4625 ),
4626 ZakuraDiscoveryConfig::default(),
4627 connected,
4628 NOW,
4629 )
4630 }
4631
4632 fn peer_id_for(node_id: NodeId) -> ZakuraPeerId {
4633 ZakuraPeerId::new(node_id.as_bytes().to_vec()).expect("node id is a valid peer id")
4634 }
4635
4636 fn header_summary() -> HeaderSyncServiceSummary {
4637 HeaderSyncServiceSummary {
4638 best_height: block::Height(42),
4639 best_hash: block::Hash([7; 32]),
4640 finalized_height: Some(block::Height(40)),
4641 serving_headers: true,
4642 inbound_slots_free: 3,
4643 inbound_slots_max: 10,
4644 outbound_slots_free: 4,
4645 outbound_slots_max: 11,
4646 }
4647 }
4648
4649 fn retired_header_sync_envelope(summary: &HeaderSyncServiceSummary) -> ServiceSummaryEnvelope {
4650 let mut summary_bytes = Vec::new();
4651 encode_header_sync_summary(summary, &mut summary_bytes)
4652 .expect("test header summary encodes");
4653 ServiceSummaryEnvelope {
4654 service_id: ZakuraServiceId::header_sync_retired(),
4655 summary_tag: SUMMARY_TAG_HEADER_SYNC_RETIRED,
4656 summary_bytes,
4657 }
4658 }
4659
4660 fn discovery_summary() -> DiscoveryServiceSummary {
4661 DiscoveryServiceSummary {
4662 peer_exchange_slots_free: 5,
4663 max_records_per_response: 12,
4664 expected_disconnect_after_exchange: true,
4665 }
4666 }
4667
4668 fn block_sync_summary() -> BlockSyncServiceSummary {
4669 BlockSyncServiceSummary {
4670 servable_low: block::Height(10),
4671 servable_high: block::Height(42),
4672 tip_hash: block::Hash([9; 32]),
4673 free_slots: 3,
4674 max_blocks_per_response: 16,
4675 max_response_bytes: MAX_BS_RESPONSE_BYTES,
4676 }
4677 }
4678
4679 fn block_sync_status() -> BlockSyncStatus {
4680 BlockSyncStatus {
4681 servable_low: block::Height(10),
4682 servable_high: block::Height(42),
4683 tip_hash: block::Hash([9; 32]),
4684 max_blocks_per_response: 16,
4685 max_inflight_requests: 4,
4686 max_response_bytes: MAX_BS_RESPONSE_BYTES,
4687 }
4688 }
4689
4690 fn services_message(summaries: Vec<ServiceSummaryEnvelope>) -> DiscoveryMessage {
4691 DiscoveryMessage::Services(Services {
4692 node_id: secret_key().public(),
4693 expires_at_unix_secs: NOW + 30,
4694 summaries,
4695 })
4696 }
4697
4698 fn first_party_services(
4699 node_id: NodeId,
4700 expires_at_unix_secs: u64,
4701 summaries: Vec<ServiceSummaryEnvelope>,
4702 ) -> Services {
4703 Services {
4704 node_id,
4705 expires_at_unix_secs,
4706 summaries,
4707 }
4708 }
4709
4710 #[test]
4711 fn service_id_accepts_valid_ids_and_rejects_invalid_ids() {
4712 assert_eq!(
4713 ZakuraServiceId::new("zakura.custom.v1").unwrap().as_str(),
4714 "zakura.custom.v1"
4715 );
4716 assert!(matches!(
4717 ZakuraServiceId::new(""),
4718 Err(DiscoveryWireError::Empty("service id"))
4719 ));
4720 assert!(matches!(
4721 ZakuraServiceId::new("zakura.\u{2603}.v1"),
4722 Err(DiscoveryWireError::NonAsciiServiceId)
4723 ));
4724 assert!(matches!(
4725 ZakuraServiceId::new("a".repeat(MAX_ZAKURA_SERVICE_ID_BYTES + 1)),
4726 Err(DiscoveryWireError::OversizedPayload { .. })
4727 ));
4728 }
4729
4730 #[test]
4731 fn block_sync_summary_uses_live_peer_slots() {
4732 let limits = ServicePeerLimits {
4733 max_inbound_peers: 2,
4734 ..ServicePeerLimits::default()
4735 };
4736 let status = block_sync_status();
4737
4738 let open = BlockSyncServiceSummary::from_status_and_snapshot(
4739 status,
4740 ServicePeerSnapshot::new(1, 0, limits),
4741 );
4742 assert_eq!(open.free_slots, 1);
4743
4744 let full = BlockSyncServiceSummary::from_status_and_snapshot(
4745 status,
4746 ServicePeerSnapshot::new(2, 0, limits),
4747 );
4748 assert_eq!(full.free_slots, 0);
4749 }
4750
4751 #[test]
4752 fn node_record_encode_decode_roundtrip() {
4753 let record = signed_record();
4754 let signed_bytes = record.body.encode_for_signature().expect("record signs");
4755 let mut signed_bytes_reader =
4756 Cursor::new(&signed_bytes[ZAKURA_NODE_RECORD_SIG_DOMAIN.len()..]);
4757 assert!(signed_bytes.starts_with(ZAKURA_NODE_RECORD_SIG_DOMAIN));
4758 assert_eq!(
4759 signed_bytes_reader.read_u16::<LittleEndian>().unwrap(),
4760 ZAKURA_NODE_RECORD_FORMAT_VERSION
4761 );
4762
4763 let mut body_bytes = Vec::new();
4764 encode_record_body_to(&record.body, &mut body_bytes).expect("record body encodes");
4765 assert_eq!(
4766 &signed_bytes[ZAKURA_NODE_RECORD_SIG_DOMAIN.len() + 2..],
4767 body_bytes.as_slice()
4768 );
4769
4770 let mut bytes = Vec::new();
4771 encode_record(&record, &mut bytes).expect("record encodes");
4772 let encoded_body_len =
4773 usize::try_from(Cursor::new(&bytes).read_u32::<LittleEndian>().unwrap())
4774 .expect("encoded record body length fits in usize");
4775 assert_eq!(encoded_body_len, body_bytes.len());
4776
4777 let decoded = decode_record(&mut Cursor::new(&bytes)).expect("record decodes");
4778
4779 assert_eq!(decoded, record);
4780 assert_eq!(decoded.body.encode_for_signature().unwrap(), signed_bytes);
4781 decoded.verify(&context()).expect("record verifies");
4782 }
4783
4784 #[test]
4785 fn discovery_message_roundtrips_every_variant() {
4786 let record = signed_record();
4787 let other = signed_record();
4788 let services = vec![ZakuraServiceId::discovery(), service(1)];
4789 let excluded = vec![record.body.node_id, other.body.node_id];
4790 let header_envelope =
4791 ServiceSummaryEnvelope::header_sync(&header_summary()).expect("summary encodes");
4792 let discovery_envelope =
4793 ServiceSummaryEnvelope::discovery(&discovery_summary()).expect("summary encodes");
4794 let block_envelope =
4795 ServiceSummaryEnvelope::block_sync(&block_sync_summary()).expect("summary encodes");
4796 let messages = vec![
4797 DiscoveryMessage::Hello {
4798 record: record.clone(),
4799 },
4800 DiscoveryMessage::GetPeers {
4801 limit: 2,
4802 wanted_services: services.clone(),
4803 exclude_node_ids: excluded.clone(),
4804 },
4805 DiscoveryMessage::Peers {
4806 records: vec![record.clone(), other.clone()],
4807 },
4808 DiscoveryMessage::GetServices(GetServices {
4809 wanted_services: services,
4810 }),
4811 services_message(vec![header_envelope, discovery_envelope, block_envelope]),
4812 ];
4813
4814 for message in messages {
4815 let encoded = message.encode().expect("message encodes");
4816 assert_eq!(DiscoveryMessage::decode(&encoded).unwrap(), message);
4817 }
4818 }
4819
4820 #[test]
4821 fn service_summary_envelopes_roundtrip_and_unknown_tags_are_skipped() {
4822 let header = header_summary();
4823 let discovery = discovery_summary();
4824 let block = block_sync_summary();
4825 let header_envelope =
4826 ServiceSummaryEnvelope::header_sync(&header).expect("header summary encodes");
4827 let discovery_envelope =
4828 ServiceSummaryEnvelope::discovery(&discovery).expect("discovery summary encodes");
4829 let block_envelope =
4830 ServiceSummaryEnvelope::block_sync(&block).expect("block summary encodes");
4831 let unknown_envelope = ServiceSummaryEnvelope {
4832 service_id: service(99),
4833 summary_tag: 999,
4834 summary_bytes: vec![1, 2, 3, 4],
4835 };
4836
4837 assert_eq!(header_envelope.decode_header_sync().unwrap(), Some(header));
4838 assert_eq!(
4839 discovery_envelope.decode_discovery().unwrap(),
4840 Some(discovery)
4841 );
4842 assert_eq!(block_envelope.decode_block_sync().unwrap(), Some(block));
4843
4844 let message = services_message(vec![
4845 unknown_envelope.clone(),
4846 header_envelope.clone(),
4847 discovery_envelope.clone(),
4848 block_envelope.clone(),
4849 ]);
4850 let decoded = DiscoveryMessage::decode(&message.encode().expect("message encodes"))
4851 .expect("message decodes");
4852
4853 let DiscoveryMessage::Services(services) = decoded else {
4854 panic!("expected service response");
4855 };
4856 assert_eq!(services.summaries[0], unknown_envelope);
4857 assert_eq!(
4858 services.summaries[1].decode_header_sync().unwrap(),
4859 Some(header)
4860 );
4861 assert_eq!(
4862 services.summaries[2].decode_discovery().unwrap(),
4863 Some(discovery)
4864 );
4865 assert_eq!(
4866 services.summaries[3].decode_block_sync().unwrap(),
4867 Some(block)
4868 );
4869 }
4870
4871 #[test]
4872 fn service_summary_decode_rejects_lying_truncated_and_oversized_lengths() {
4873 let node_id = secret_key().public();
4874 let mut lying = vec![MSG_DISCOVERY_SERVICES];
4875 lying.write_all(node_id.as_bytes()).unwrap();
4876 lying.write_u64::<LittleEndian>(NOW + 30).unwrap();
4877 lying.write_u16::<LittleEndian>(1).unwrap();
4878 encode_service_id(&service(1), &mut lying).unwrap();
4879 lying.write_u16::<LittleEndian>(999).unwrap();
4880 lying.write_u16::<LittleEndian>(4).unwrap();
4881 lying.write_u8(1).unwrap();
4882
4883 assert!(matches!(
4884 DiscoveryMessage::decode(&lying),
4885 Err(DiscoveryWireError::TruncatedPayload { .. })
4886 ));
4887
4888 let mut truncated_known = vec![MSG_DISCOVERY_SERVICES];
4889 truncated_known.write_all(node_id.as_bytes()).unwrap();
4890 truncated_known.write_u64::<LittleEndian>(NOW + 30).unwrap();
4891 truncated_known.write_u16::<LittleEndian>(1).unwrap();
4892 encode_service_id(&ZakuraServiceId::header_sync(), &mut truncated_known).unwrap();
4893 truncated_known
4894 .write_u16::<LittleEndian>(SUMMARY_TAG_HEADER_SYNC)
4895 .unwrap();
4896 truncated_known.write_u16::<LittleEndian>(1).unwrap();
4897 truncated_known.write_u8(0).unwrap();
4898
4899 assert!(DiscoveryMessage::decode(&truncated_known).is_err());
4900
4901 let mut truncated_block = vec![MSG_DISCOVERY_SERVICES];
4902 truncated_block.write_all(node_id.as_bytes()).unwrap();
4903 truncated_block.write_u64::<LittleEndian>(NOW + 30).unwrap();
4904 truncated_block.write_u16::<LittleEndian>(1).unwrap();
4905 encode_service_id(&ZakuraServiceId::block_sync(), &mut truncated_block).unwrap();
4906 truncated_block
4907 .write_u16::<LittleEndian>(SUMMARY_TAG_BLOCK_SYNC_V1)
4908 .unwrap();
4909 truncated_block.write_u16::<LittleEndian>(8).unwrap();
4910 write_height(&mut truncated_block, block::Height(10)).unwrap();
4911 write_height(&mut truncated_block, block::Height(42)).unwrap();
4912
4913 assert!(DiscoveryMessage::decode(&truncated_block).is_err());
4914
4915 let mut oversized = vec![MSG_DISCOVERY_SERVICES];
4916 oversized.write_all(node_id.as_bytes()).unwrap();
4917 oversized.write_u64::<LittleEndian>(NOW + 30).unwrap();
4918 oversized.write_u16::<LittleEndian>(1).unwrap();
4919 encode_service_id(&ZakuraServiceId::block_sync(), &mut oversized).unwrap();
4920 oversized
4921 .write_u16::<LittleEndian>(SUMMARY_TAG_BLOCK_SYNC_V1)
4922 .unwrap();
4923 oversized
4924 .write_u16::<LittleEndian>(
4925 u16::try_from(MAX_SERVICE_SUMMARY_BYTES + 1).expect("test length fits"),
4926 )
4927 .unwrap();
4928
4929 assert!(matches!(
4930 DiscoveryMessage::decode(&oversized),
4931 Err(DiscoveryWireError::OversizedPayload { .. })
4932 ));
4933 }
4934
4935 #[test]
4936 fn known_service_summary_payload_trailing_bytes_are_rejected() {
4937 let mut envelope =
4938 ServiceSummaryEnvelope::header_sync(&header_summary()).expect("summary encodes");
4939 envelope.summary_bytes.push(0);
4940
4941 assert!(matches!(
4942 services_message(vec![envelope.clone()]).encode(),
4943 Err(DiscoveryWireError::TrailingBytes)
4944 ));
4945
4946 let node_id = secret_key().public();
4947 let mut encoded = vec![MSG_DISCOVERY_SERVICES];
4948 encoded.write_all(node_id.as_bytes()).unwrap();
4949 encoded.write_u64::<LittleEndian>(NOW + 30).unwrap();
4950 encoded.write_u16::<LittleEndian>(1).unwrap();
4951 encode_service_id(&envelope.service_id, &mut encoded).unwrap();
4952 encoded
4953 .write_u16::<LittleEndian>(envelope.summary_tag)
4954 .unwrap();
4955 encoded
4956 .write_u16::<LittleEndian>(
4957 u16::try_from(envelope.summary_bytes.len()).expect("test length fits"),
4958 )
4959 .unwrap();
4960 encoded.write_all(&envelope.summary_bytes).unwrap();
4961
4962 assert!(matches!(
4963 DiscoveryMessage::decode(&encoded),
4964 Err(DiscoveryWireError::TrailingBytes)
4965 ));
4966
4967 let mut block_envelope =
4968 ServiceSummaryEnvelope::block_sync(&block_sync_summary()).expect("summary encodes");
4969 block_envelope.summary_bytes.push(0);
4970
4971 assert!(matches!(
4972 services_message(vec![block_envelope]).encode(),
4973 Err(DiscoveryWireError::TrailingBytes)
4974 ));
4975 }
4976
4977 #[test]
4978 fn signature_verifies_for_author_and_fails_after_mutation() {
4979 let record = signed_record();
4980 record.verify(&context()).expect("record verifies");
4981 assert!(record
4982 .body
4983 .encode_for_signature()
4984 .expect("record pre-image encodes")
4985 .starts_with(ZAKURA_NODE_RECORD_SIG_DOMAIN));
4986
4987 let mutations: Vec<
4988 Box<dyn FnOnce(&mut ZakuraNodeRecord, &mut DiscoveryRecordValidationContext)>,
4989 > = vec![
4990 Box::new(|record, _context| {
4991 record
4992 .body
4993 .direct_addrs
4994 .push("198.51.100.1:8233".parse().unwrap())
4995 }),
4996 Box::new(|record, _context| record.body.services.push(service(9))),
4997 Box::new(|record, _context| record.body.zakura_protocol_min = 0),
4998 Box::new(|record, _context| record.body.zakura_protocol_max = 2),
4999 Box::new(|record, context| {
5000 record.body.network_id = ZakuraNetworkId::Mainnet;
5001 context.expected_network_id = ZakuraNetworkId::Mainnet;
5002 }),
5003 Box::new(|record, context| {
5004 record.body.chain_id[0] ^= 1;
5005 context.expected_chain_id = record.body.chain_id;
5006 }),
5007 Box::new(|record, _context| record.body.sequence += 1),
5008 Box::new(|record, _context| record.body.expires_at_unix_secs += 1),
5009 ];
5010
5011 for mutate in mutations {
5012 let mut mutated = record.clone();
5013 let mut context = context();
5014 mutate(&mut mutated, &mut context);
5015 assert!(matches!(
5016 mutated.verify(&context),
5017 Err(DiscoveryRecordError::InvalidSignature)
5018 ));
5019 }
5020
5021 let other_secret = secret_key();
5022 let mut mutated = record;
5023 mutated.body.node_id = other_secret.public();
5024 assert!(matches!(
5025 mutated.verify(&context()),
5026 Err(DiscoveryRecordError::InvalidSignature)
5027 ));
5028 }
5029
5030 #[test]
5031 fn signature_fails_with_wrong_domain_or_record_format_version() {
5032 let secret_key = secret_key();
5033 let body = body(&secret_key);
5034
5035 let wrong_domain = sign_record_with_prefix(
5036 body.clone(),
5037 &secret_key,
5038 b"zakura-other-record-v1",
5039 ZAKURA_NODE_RECORD_FORMAT_VERSION,
5040 );
5041 assert!(matches!(
5042 wrong_domain.verify(&context()),
5043 Err(DiscoveryRecordError::InvalidSignature)
5044 ));
5045
5046 let wrong_version = sign_record_with_prefix(
5047 body,
5048 &secret_key,
5049 ZAKURA_NODE_RECORD_SIG_DOMAIN,
5050 ZAKURA_NODE_RECORD_FORMAT_VERSION + 1,
5051 );
5052 assert!(matches!(
5053 wrong_version.verify(&context()),
5054 Err(DiscoveryRecordError::InvalidSignature)
5055 ));
5056 }
5057
5058 #[test]
5059 fn discovery_network_id_decode_matches_handshake_wire_codes() {
5060 for network_id in [
5061 ZakuraNetworkId::Mainnet,
5062 ZakuraNetworkId::Testnet,
5063 ZakuraNetworkId::Regtest,
5064 ZakuraNetworkId::Configured,
5065 ] {
5066 assert_eq!(
5067 network_id_from_code(network_id.code()).expect("network id code is valid"),
5068 network_id
5069 );
5070 }
5071 }
5072
5073 #[test]
5074 fn record_validation_rejects_expired_wrong_network_wrong_chain_and_protocol() {
5075 let record = signed_record();
5076
5077 let mut expired = record.clone();
5078 expired.body.expires_at_unix_secs = NOW - context().clock_skew_tolerance.as_secs() - 1;
5079 assert!(matches!(
5080 expired.verify(&context()),
5081 Err(DiscoveryRecordError::Expired)
5082 ));
5083
5084 let mut wrong_network_context = context();
5085 wrong_network_context.expected_network_id = ZakuraNetworkId::Mainnet;
5086 assert!(matches!(
5087 record.verify(&wrong_network_context),
5088 Err(DiscoveryRecordError::WrongNetwork)
5089 ));
5090
5091 let mut wrong_chain_context = context();
5092 wrong_chain_context.expected_chain_id[0] ^= 1;
5093 assert!(matches!(
5094 record.verify(&wrong_chain_context),
5095 Err(DiscoveryRecordError::WrongChain)
5096 ));
5097
5098 let mut incompatible_context = context();
5099 incompatible_context.supported_protocol_min = 2;
5100 incompatible_context.supported_protocol_max = 2;
5101 assert!(matches!(
5102 record.verify(&incompatible_context),
5103 Err(DiscoveryRecordError::IncompatibleProtocol)
5104 ));
5105 }
5106
5107 #[test]
5108 fn record_bounds_reject_too_many_addresses_and_services() {
5109 let secret_key = secret_key();
5110 let mut too_many_addrs = body(&secret_key);
5111 too_many_addrs.direct_addrs =
5112 vec!["192.0.2.1:8233".parse().unwrap(); MAX_DIRECT_ADDRS_PER_RECORD + 1];
5113 assert!(ZakuraNodeRecord::sign(too_many_addrs, &secret_key).is_err());
5114
5115 let mut too_many_services = body(&secret_key);
5116 too_many_services.services = (0..=MAX_SERVICES_PER_RECORD).map(service).collect();
5117 assert!(ZakuraNodeRecord::sign(too_many_services, &secret_key).is_err());
5118 }
5119
5120 #[test]
5121 fn response_and_query_bounds_are_enforced() {
5122 let records = vec![signed_record(); MAX_DISCOVERY_RECORDS_PER_RESPONSE + 1];
5123 assert!(DiscoveryMessage::Peers {
5124 records: records.clone()
5125 }
5126 .encode()
5127 .is_err());
5128
5129 let wanted_services = (0..=MAX_SERVICES_PER_RECORD).map(service).collect();
5130 assert!(DiscoveryMessage::GetPeers {
5131 limit: 1,
5132 wanted_services,
5133 exclude_node_ids: Vec::new(),
5134 }
5135 .encode()
5136 .is_err());
5137
5138 let wanted_services = (0..=MAX_GET_SERVICES_WANTED).map(service).collect();
5139 assert!(
5140 DiscoveryMessage::GetServices(GetServices { wanted_services })
5141 .encode()
5142 .is_err()
5143 );
5144
5145 assert!(DiscoveryMessage::GetPeers {
5146 limit: u16::try_from(MAX_DISCOVERY_RECORDS_PER_RESPONSE + 1)
5147 .expect("test limit fits in u16"),
5148 wanted_services: Vec::new(),
5149 exclude_node_ids: Vec::new(),
5150 }
5151 .encode()
5152 .is_err());
5153
5154 let too_many_summaries = vec![
5155 ServiceSummaryEnvelope {
5156 service_id: service(1),
5157 summary_tag: 999,
5158 summary_bytes: Vec::new(),
5159 };
5160 MAX_SERVICE_SUMMARIES_PER_RESPONSE + 1
5161 ];
5162 assert!(services_message(too_many_summaries).encode().is_err());
5163
5164 let oversized_summary = ServiceSummaryEnvelope {
5165 service_id: service(1),
5166 summary_tag: 999,
5167 summary_bytes: vec![0; MAX_SERVICE_SUMMARY_BYTES + 1],
5168 };
5169 assert!(services_message(vec![oversized_summary]).encode().is_err());
5170
5171 let large_summary = ServiceSummaryEnvelope {
5172 service_id: service(1),
5173 summary_tag: 999,
5174 summary_bytes: vec![0; MAX_SERVICE_SUMMARY_BYTES],
5175 };
5176 let total_oversized = vec![large_summary; 16];
5177 assert!(matches!(
5178 services_message(total_oversized).encode(),
5179 Err(DiscoveryWireError::OversizedPayload { .. })
5180 ));
5181 }
5182
5183 #[test]
5190 fn retired_header_sync_summary_from_a_pre_upgrade_peer_decodes_as_advisory() {
5191 let summary = header_summary();
5192 let retired = retired_header_sync_envelope(&summary);
5193
5194 let encoded = services_message(vec![retired.clone()])
5195 .encode()
5196 .expect("a pre-upgrade peer's header-sync summary must still encode");
5197 assert!(
5198 DiscoveryMessage::decode(&encoded).is_ok(),
5199 "a pre-upgrade peer's Services message must not be a wire protocol violation"
5200 );
5201
5202 assert_eq!(
5203 retired
5204 .decode_header_sync()
5205 .expect("the retired payload is well-formed"),
5206 Some(summary),
5207 "a retired-generation summary remains an advisory payload"
5208 );
5209 }
5210
5211 #[test]
5212 fn known_summary_tags_must_match_their_service_ids() {
5213 let mismatched_header = ServiceSummaryEnvelope {
5214 service_id: ZakuraServiceId::discovery(),
5215 summary_tag: SUMMARY_TAG_HEADER_SYNC,
5216 summary_bytes: {
5217 let mut bytes = Vec::new();
5218 encode_header_sync_summary(&header_summary(), &mut bytes)
5219 .expect("test header summary encodes");
5220 bytes
5221 },
5222 };
5223 assert!(matches!(
5224 services_message(vec![mismatched_header.clone()]).encode(),
5225 Err(DiscoveryWireError::MismatchedSummaryService {
5226 summary_tag: SUMMARY_TAG_HEADER_SYNC,
5227 ..
5228 })
5229 ));
5230 let mut mismatched_header_wire = vec![MSG_DISCOVERY_SERVICES];
5231 mismatched_header_wire
5232 .write_all(secret_key().public().as_bytes())
5233 .expect("test node id writes");
5234 mismatched_header_wire
5235 .write_u64::<LittleEndian>(NOW + 30)
5236 .expect("test expiry writes");
5237 mismatched_header_wire
5238 .write_u16::<LittleEndian>(1)
5239 .expect("test summary count writes");
5240 encode_service_id(&ZakuraServiceId::discovery(), &mut mismatched_header_wire)
5241 .expect("test service id writes");
5242 mismatched_header_wire
5243 .write_u16::<LittleEndian>(SUMMARY_TAG_HEADER_SYNC)
5244 .expect("test summary tag writes");
5245 mismatched_header_wire
5246 .write_u16::<LittleEndian>(
5247 u16::try_from(mismatched_header.summary_bytes.len())
5248 .expect("test summary length fits in u16"),
5249 )
5250 .expect("test summary length writes");
5251 mismatched_header_wire
5252 .write_all(&mismatched_header.summary_bytes)
5253 .expect("test summary payload writes");
5254 assert!(matches!(
5255 DiscoveryMessage::decode(&mismatched_header_wire),
5256 Err(DiscoveryWireError::MismatchedSummaryService {
5257 summary_tag: SUMMARY_TAG_HEADER_SYNC,
5258 ..
5259 })
5260 ));
5261
5262 let mismatched_discovery = ServiceSummaryEnvelope {
5263 service_id: ZakuraServiceId::header_sync(),
5264 summary_tag: SUMMARY_TAG_DISCOVERY_V1,
5265 summary_bytes: {
5266 let mut bytes = Vec::new();
5267 encode_discovery_summary(&discovery_summary(), &mut bytes)
5268 .expect("test discovery summary encodes");
5269 bytes
5270 },
5271 };
5272 assert!(matches!(
5273 services_message(vec![mismatched_discovery]).encode(),
5274 Err(DiscoveryWireError::MismatchedSummaryService {
5275 summary_tag: SUMMARY_TAG_DISCOVERY_V1,
5276 ..
5277 })
5278 ));
5279
5280 let mismatched_block = ServiceSummaryEnvelope {
5281 service_id: ZakuraServiceId::header_sync(),
5282 summary_tag: SUMMARY_TAG_BLOCK_SYNC_V1,
5283 summary_bytes: {
5284 let mut bytes = Vec::new();
5285 encode_block_sync_summary(&block_sync_summary(), &mut bytes)
5286 .expect("test block summary encodes");
5287 bytes
5288 },
5289 };
5290 assert!(matches!(
5291 services_message(vec![mismatched_block]).encode(),
5292 Err(DiscoveryWireError::MismatchedSummaryService {
5293 summary_tag: SUMMARY_TAG_BLOCK_SYNC_V1,
5294 ..
5295 })
5296 ));
5297 }
5298
5299 #[test]
5300 fn decode_rejects_too_many_records_services_and_excluded_ids_before_allocation() {
5301 let mut peers = vec![MSG_DISCOVERY_PEERS];
5302 peers
5303 .write_u16::<LittleEndian>(
5304 u16::try_from(MAX_DISCOVERY_RECORDS_PER_RESPONSE + 1)
5305 .expect("test record count fits in u16"),
5306 )
5307 .unwrap();
5308 assert!(matches!(
5309 DiscoveryMessage::decode(&peers),
5310 Err(DiscoveryWireError::OversizedPayload { .. })
5311 ));
5312
5313 let mut query = vec![MSG_DISCOVERY_GET_PEERS];
5314 query.write_u16::<LittleEndian>(1).unwrap();
5315 query
5316 .write_u16::<LittleEndian>(
5317 u16::try_from(MAX_SERVICES_PER_RECORD + 1).expect("test service count fits in u16"),
5318 )
5319 .unwrap();
5320 assert!(matches!(
5321 DiscoveryMessage::decode(&query),
5322 Err(DiscoveryWireError::OversizedPayload { .. })
5323 ));
5324
5325 let mut query = vec![MSG_DISCOVERY_GET_PEERS];
5326 query.write_u16::<LittleEndian>(1).unwrap();
5327 query.write_u16::<LittleEndian>(0).unwrap();
5328 query
5329 .write_u16::<LittleEndian>(
5330 u16::try_from(MAX_DISCOVERY_EXCLUDED_NODE_IDS + 1)
5331 .expect("test excluded count fits in u16"),
5332 )
5333 .unwrap();
5334 assert!(matches!(
5335 DiscoveryMessage::decode(&query),
5336 Err(DiscoveryWireError::OversizedPayload { .. })
5337 ));
5338
5339 let mut service_query = vec![MSG_DISCOVERY_GET_SERVICES];
5340 service_query
5341 .write_u16::<LittleEndian>(
5342 u16::try_from(MAX_GET_SERVICES_WANTED + 1).expect("test service count fits in u16"),
5343 )
5344 .unwrap();
5345 assert!(matches!(
5346 DiscoveryMessage::decode(&service_query),
5347 Err(DiscoveryWireError::OversizedPayload { .. })
5348 ));
5349
5350 let mut service_response = vec![MSG_DISCOVERY_SERVICES];
5351 service_response
5352 .write_all(secret_key().public().as_bytes())
5353 .unwrap();
5354 service_response
5355 .write_u64::<LittleEndian>(NOW + 30)
5356 .unwrap();
5357 service_response
5358 .write_u16::<LittleEndian>(
5359 u16::try_from(MAX_SERVICE_SUMMARIES_PER_RESPONSE + 1)
5360 .expect("test summary count fits in u16"),
5361 )
5362 .unwrap();
5363 assert!(matches!(
5364 DiscoveryMessage::decode(&service_response),
5365 Err(DiscoveryWireError::OversizedPayload { .. })
5366 ));
5367
5368 let mut oversized_summary = vec![MSG_DISCOVERY_SERVICES];
5369 oversized_summary
5370 .write_all(secret_key().public().as_bytes())
5371 .unwrap();
5372 oversized_summary
5373 .write_u64::<LittleEndian>(NOW + 30)
5374 .unwrap();
5375 oversized_summary.write_u16::<LittleEndian>(1).unwrap();
5376 encode_service_id(&service(1), &mut oversized_summary).unwrap();
5377 oversized_summary.write_u16::<LittleEndian>(999).unwrap();
5378 oversized_summary
5379 .write_u16::<LittleEndian>(
5380 u16::try_from(MAX_SERVICE_SUMMARY_BYTES + 1).expect("summary cap fits in u16"),
5381 )
5382 .unwrap();
5383 assert!(matches!(
5384 DiscoveryMessage::decode(&oversized_summary),
5385 Err(DiscoveryWireError::OversizedPayload { .. })
5386 ));
5387 }
5388
5389 #[test]
5390 fn peers_response_cannot_encode_live_summaries_structurally() {
5391 let message = DiscoveryMessage::Peers {
5392 records: vec![signed_record()],
5393 };
5394 let encoded = message.encode().expect("peers message encodes");
5395 let decoded = DiscoveryMessage::decode(&encoded).expect("peers message decodes");
5396
5397 match decoded {
5398 DiscoveryMessage::Peers { records } => assert_eq!(records.len(), 1),
5399 DiscoveryMessage::Services(_) => panic!("PEERS decoded as SERVICES"),
5400 _ => panic!("unexpected discovery message variant"),
5401 }
5402 }
5403
5404 #[test]
5405 fn decode_rejects_malformed_record_body_counts_before_allocation() {
5406 let record = signed_record();
5407 let mut body_bytes = Vec::new();
5408 body_bytes
5409 .write_all(record.body.node_id.as_bytes())
5410 .unwrap();
5411 body_bytes
5412 .write_u16::<LittleEndian>(
5413 u16::try_from(MAX_DIRECT_ADDRS_PER_RECORD + 1)
5414 .expect("test address count fits in u16"),
5415 )
5416 .unwrap();
5417
5418 let mut encoded = vec![MSG_DISCOVERY_HELLO];
5419 encoded
5420 .write_u32::<LittleEndian>(
5421 u32::try_from(body_bytes.len()).expect("test body length fits in u32"),
5422 )
5423 .unwrap();
5424 encoded.write_all(&body_bytes).unwrap();
5425 encoded.write_all(&record.signature.to_bytes()).unwrap();
5426
5427 assert!(matches!(
5428 DiscoveryMessage::decode(&encoded),
5429 Err(DiscoveryWireError::OversizedPayload { .. })
5430 ));
5431
5432 let record = signed_record();
5433 let mut body_bytes = Vec::new();
5434 body_bytes
5435 .write_all(record.body.node_id.as_bytes())
5436 .unwrap();
5437 body_bytes.write_u16::<LittleEndian>(0).unwrap();
5438 body_bytes
5439 .write_u16::<LittleEndian>(
5440 u16::try_from(MAX_SERVICES_PER_RECORD + 1).expect("test service count fits in u16"),
5441 )
5442 .unwrap();
5443
5444 let mut encoded = vec![MSG_DISCOVERY_HELLO];
5445 encoded
5446 .write_u32::<LittleEndian>(
5447 u32::try_from(body_bytes.len()).expect("test body length fits in u32"),
5448 )
5449 .unwrap();
5450 encoded.write_all(&body_bytes).unwrap();
5451 encoded.write_all(&record.signature.to_bytes()).unwrap();
5452
5453 assert!(matches!(
5454 DiscoveryMessage::decode(&encoded),
5455 Err(DiscoveryWireError::OversizedPayload { .. })
5456 ));
5457 }
5458
5459 #[test]
5460 fn signature_fails_when_signed_vectors_are_reordered() {
5461 let record = signed_record();
5462 let signed_bytes = record
5463 .body
5464 .encode_for_signature()
5465 .expect("record pre-image encodes");
5466
5467 let mut reordered_services = record.clone();
5468 reordered_services.body.services.swap(0, 1);
5469 assert_ne!(
5470 reordered_services
5471 .body
5472 .encode_for_signature()
5473 .expect("reordered services pre-image encodes"),
5474 signed_bytes
5475 );
5476 assert!(matches!(
5477 reordered_services.verify(&context()),
5478 Err(DiscoveryRecordError::InvalidSignature)
5479 ));
5480
5481 let mut reordered_addrs = record;
5482 reordered_addrs.body.direct_addrs.swap(0, 1);
5483 assert_ne!(
5484 reordered_addrs
5485 .body
5486 .encode_for_signature()
5487 .expect("reordered addrs pre-image encodes"),
5488 signed_bytes
5489 );
5490 assert!(matches!(
5491 reordered_addrs.verify(&context()),
5492 Err(DiscoveryRecordError::InvalidSignature)
5493 ));
5494 }
5495
5496 #[test]
5497 fn far_future_expiry_and_clock_skew_edges_are_enforced() {
5498 let mut record = signed_record();
5499
5500 record.body.expires_at_unix_secs =
5501 NOW + context().max_record_ttl.as_secs() + context().clock_skew_tolerance.as_secs();
5502 let signing_secret = secret_key();
5503 record = ZakuraNodeRecord::sign(
5504 ZakuraNodeRecordBody {
5505 node_id: signing_secret.public(),
5506 ..record.body
5507 },
5508 &signing_secret,
5509 )
5510 .expect("record signs");
5511 record.verify(&context()).expect("skew edge is accepted");
5512
5513 let signing_secret = secret_key();
5514 let mut too_far = body(&signing_secret);
5515 too_far.expires_at_unix_secs =
5516 NOW + context().max_record_ttl.as_secs() + context().clock_skew_tolerance.as_secs() + 1;
5517 let too_far = ZakuraNodeRecord::sign(too_far, &signing_secret).expect("record signs");
5518 assert!(matches!(
5519 too_far.verify(&context()),
5520 Err(DiscoveryRecordError::FarFutureExpiry)
5521 ));
5522
5523 let signing_secret = secret_key();
5524 let mut just_expired = body(&signing_secret);
5525 just_expired.expires_at_unix_secs = NOW - context().clock_skew_tolerance.as_secs();
5526 let just_expired =
5527 ZakuraNodeRecord::sign(just_expired, &signing_secret).expect("record signs");
5528 just_expired
5529 .verify(&context())
5530 .expect("just expired within skew is accepted");
5531 }
5532
5533 #[test]
5534 fn decode_rejects_trailing_and_unknown_bytes() {
5535 let mut encoded = DiscoveryMessage::Hello {
5536 record: signed_record(),
5537 }
5538 .encode()
5539 .expect("message encodes");
5540 encoded.push(0);
5541 assert!(matches!(
5542 DiscoveryMessage::decode(&encoded),
5543 Err(DiscoveryWireError::TrailingBytes)
5544 ));
5545
5546 assert!(matches!(
5547 DiscoveryMessage::decode(&[99]),
5548 Err(DiscoveryWireError::InvalidMessageType(99))
5549 ));
5550 }
5551
5552 #[test]
5553 fn discovery_book_imports_valid_record() {
5554 let mut book = ZakuraDiscoveryBook::default();
5555 let source = secret_key().public();
5556 let record = signed_record();
5557 let node_id = record.body.node_id;
5558
5559 assert_eq!(
5560 book.import_record(record.clone(), Some(source), NOW, &context())
5561 .expect("valid record imports"),
5562 ImportOutcome::Added
5563 );
5564
5565 let entry = book.get(&node_id).expect("entry was inserted");
5566 assert_eq!(entry.record(), &record);
5567 assert_eq!(entry.source(), Some(source));
5568 assert_eq!(entry.last_seen(), NOW);
5569 assert!(!entry.is_static());
5570 }
5571
5572 #[test]
5573 fn discovery_book_rejects_invalid_signature_expired_wrong_network_and_wrong_chain() {
5574 let mut book = ZakuraDiscoveryBook::default();
5575
5576 let mut invalid_signature = signed_record();
5577 invalid_signature.body.sequence += 1;
5578 assert!(matches!(
5579 book.import_record(invalid_signature, None, NOW, &context()),
5580 Err(DiscoveryBookError::Record(
5581 DiscoveryRecordError::InvalidSignature
5582 ))
5583 ));
5584
5585 let signing_secret = secret_key();
5586 let mut expired = body(&signing_secret);
5587 expired.expires_at_unix_secs = NOW - context().clock_skew_tolerance.as_secs() - 1;
5588 let expired = ZakuraNodeRecord::sign(expired, &signing_secret).expect("record signs");
5589 assert!(matches!(
5590 book.import_record(expired, None, NOW, &context()),
5591 Err(DiscoveryBookError::Record(DiscoveryRecordError::Expired))
5592 ));
5593
5594 let mut wrong_network_context = context();
5595 wrong_network_context.expected_network_id = ZakuraNetworkId::Mainnet;
5596 assert!(matches!(
5597 book.import_record(signed_record(), None, NOW, &wrong_network_context),
5598 Err(DiscoveryBookError::Record(
5599 DiscoveryRecordError::WrongNetwork
5600 ))
5601 ));
5602
5603 let mut wrong_chain_context = context();
5604 wrong_chain_context.expected_chain_id[0] ^= 1;
5605 assert!(matches!(
5606 book.import_record(signed_record(), None, NOW, &wrong_chain_context),
5607 Err(DiscoveryBookError::Record(DiscoveryRecordError::WrongChain))
5608 ));
5609 }
5610
5611 #[test]
5612 fn discovery_book_rejects_self_record_no_usable_address_and_far_future_expiry() {
5613 let local_secret = secret_key();
5614 let local_record = record_with_secret(&local_secret, 1, service(1));
5615 let mut book = ZakuraDiscoveryBook::with_local_node_id(
5616 ZakuraDiscoveryBookLimits::default(),
5617 local_secret.public(),
5618 );
5619
5620 assert!(matches!(
5621 book.import_record(local_record.clone(), None, NOW, &context()),
5622 Err(DiscoveryBookError::SelfRecord)
5623 ));
5624 assert!(book.get(&local_secret.public()).is_none());
5625
5626 let signing_secret = secret_key();
5627 let mut no_addr = body(&signing_secret);
5628 no_addr.direct_addrs = Vec::new();
5629 let no_addr = ZakuraNodeRecord::sign(no_addr, &signing_secret).expect("record signs");
5630 assert!(matches!(
5631 book.import_record(no_addr, None, NOW, &context()),
5632 Err(DiscoveryBookError::NoUsableDirectAddress)
5633 ));
5634
5635 let signing_secret = secret_key();
5636 let mut too_far = body(&signing_secret);
5637 too_far.expires_at_unix_secs =
5638 NOW + context().max_record_ttl.as_secs() + context().clock_skew_tolerance.as_secs() + 1;
5639 let too_far = ZakuraNodeRecord::sign(too_far, &signing_secret).expect("record signs");
5640 assert!(matches!(
5641 book.import_record(too_far, None, NOW, &context()),
5642 Err(DiscoveryBookError::Record(
5643 DiscoveryRecordError::FarFutureExpiry
5644 ))
5645 ));
5646 }
5647
5648 #[test]
5649 fn discovery_book_rejects_non_dialable_direct_addresses() {
5650 let bad_addrs = [
5651 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8233),
5652 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8233),
5653 SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8233),
5654 SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8233),
5655 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), 8233),
5656 SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1)), 8233),
5657 SocketAddr::new(IpAddr::V4(Ipv4Addr::BROADCAST), 8233),
5658 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(169, 254, 1, 1)), 8233),
5659 SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)), 8233),
5660 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 20)), 0),
5661 ];
5662
5663 for (index, bad_addr) in bad_addrs.into_iter().enumerate() {
5664 let mut book = ZakuraDiscoveryBook::default();
5665 let record = signed_record_with_addrs(
5666 u64::try_from(index).expect("small test index fits in u64"),
5667 service(index),
5668 vec![test_addr(20), bad_addr],
5669 );
5670
5671 assert!(matches!(
5672 book.import_record(record, None, NOW, &context()),
5673 Err(DiscoveryBookError::NonDialableDirectAddress { addr }) if addr == bad_addr
5674 ));
5675 assert!(book.is_empty());
5676 }
5677 }
5678
5679 #[test]
5680 fn discovery_book_peer_import_rejects_private_and_internal_direct_addresses() {
5681 let bad_addrs = [
5688 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5)), 8233),
5689 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 16, 5, 5)), 8233),
5690 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8233),
5691 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)), 8233),
5692 SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1)), 8233),
5693 SocketAddr::new(
5694 IpAddr::V6(Ipv6Addr::new(0xfd12, 0x3456, 0, 0, 0, 0, 0, 1)),
5695 8233,
5696 ),
5697 ];
5698
5699 for (index, bad_addr) in bad_addrs.into_iter().enumerate() {
5700 let mut book = ZakuraDiscoveryBook::default();
5701 let record = signed_record_with_addrs(
5704 u64::try_from(index).expect("small test index fits in u64"),
5705 service(index),
5706 vec![test_addr(30), bad_addr],
5707 );
5708
5709 assert!(
5710 matches!(
5711 book.import_record(record, Some(secret_key().public()), NOW, &context()),
5712 Err(DiscoveryBookError::NonDialableDirectAddress { addr }) if addr == bad_addr
5713 ),
5714 "untrusted gossip must reject internal direct address {bad_addr}"
5715 );
5716 assert!(book.is_empty());
5717 }
5718
5719 let mut book = ZakuraDiscoveryBook::default();
5721 let public_record = signed_record_with_addrs(99, service(99), vec![test_addr(40)]);
5722 assert_eq!(
5723 book.import_record(public_record, Some(secret_key().public()), NOW, &context())
5724 .expect("public gossip record imports"),
5725 ImportOutcome::Added
5726 );
5727
5728 let mut static_book = ZakuraDiscoveryBook::default();
5731 let private_static = signed_record_with_addrs(
5732 1,
5733 service(1),
5734 vec![SocketAddr::new(
5735 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)),
5736 8233,
5737 )],
5738 );
5739 assert_eq!(
5740 static_book
5741 .import_static_record(private_static, NOW, &context())
5742 .expect("configured static private record imports"),
5743 ImportOutcome::Added
5744 );
5745 }
5746
5747 #[test]
5748 fn discovery_book_gossiped_public_third_party_record_is_not_dialable() {
5749 let mut book = ZakuraDiscoveryBook::default();
5750 let gossip_source = secret_key().public();
5751 let public_target = signed_record_with_addrs(
5752 1,
5753 service(1),
5754 vec![SocketAddr::new(
5755 IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)),
5756 8233,
5757 )],
5758 );
5759 let target_id = public_target.body.node_id;
5760
5761 assert_eq!(
5762 book.import_record(public_target.clone(), Some(gossip_source), NOW, &context())
5763 .expect("valid public third-party gossip record still imports"),
5764 ImportOutcome::Added
5765 );
5766 assert_eq!(
5767 book.get(&target_id).expect("entry is stored").record(),
5768 &public_target
5769 );
5770
5771 assert!(
5772 book.dial_candidates(
5773 10,
5774 &[service(1)],
5775 DialCandidateExclusions {
5776 connected_node_ids: &[],
5777 in_flight_node_ids: &[],
5778 },
5779 NOW,
5780 (
5781 DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE,
5782 DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX,
5783 ),
5784 &mut StdRng::seed_from_u64(7),
5785 )
5786 .is_empty(),
5787 "unconfirmed third-party public gossip must not drive native dials"
5788 );
5789
5790 assert_eq!(
5791 book.import_record(public_target.clone(), Some(target_id), NOW + 1, &context())
5792 .expect("first-party self-record confirmation imports"),
5793 ImportOutcome::MetadataUpdated
5794 );
5795 assert_eq!(
5796 book.dial_candidates(
5797 10,
5798 &[service(1)],
5799 DialCandidateExclusions {
5800 connected_node_ids: &[],
5801 in_flight_node_ids: &[],
5802 },
5803 NOW + 1,
5804 (
5805 DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE,
5806 DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX,
5807 ),
5808 &mut StdRng::seed_from_u64(7),
5809 ),
5810 vec![candidate_for(&public_target, false)]
5811 );
5812 }
5813
5814 #[test]
5815 fn discovery_book_static_import_allows_loopback_direct_address() {
5816 let mut book = ZakuraDiscoveryBook::default();
5817 let loopback_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8233);
5818 let record = signed_record_with_addrs(1, service(1), vec![loopback_addr]);
5819 let node_id = record.body.node_id;
5820
5821 assert_eq!(
5822 book.import_static_record(record.clone(), NOW, &context())
5823 .expect("configured static loopback record imports"),
5824 ImportOutcome::Added
5825 );
5826
5827 let entry = book
5828 .get(&node_id)
5829 .expect("static loopback entry was stored");
5830 assert!(entry.is_static());
5831 assert_eq!(entry.record(), &record);
5832 }
5833
5834 #[test]
5835 fn discovery_book_peer_import_still_rejects_loopback_direct_address() {
5836 let mut book = ZakuraDiscoveryBook::default();
5837 let loopback_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8233);
5838 let record = signed_record_with_addrs(1, service(1), vec![loopback_addr]);
5839
5840 assert!(matches!(
5841 book.import_record(record, Some(secret_key().public()), NOW, &context()),
5842 Err(DiscoveryBookError::NonDialableDirectAddress { addr }) if addr == loopback_addr
5843 ));
5844 assert!(book.is_empty());
5845 }
5846
5847 #[test]
5848 fn discovery_book_static_import_rejects_unspecified_and_port_zero_addresses() {
5849 let bad_addrs = [
5850 SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8233),
5851 SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8233),
5852 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), 8233),
5853 SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1)), 8233),
5854 SocketAddr::new(IpAddr::V4(Ipv4Addr::BROADCAST), 8233),
5855 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
5856 ];
5857
5858 for (index, bad_addr) in bad_addrs.into_iter().enumerate() {
5859 let mut book = ZakuraDiscoveryBook::default();
5860 let record = signed_record_with_addrs(
5861 u64::try_from(index).expect("small test index fits in u64"),
5862 service(index),
5863 vec![bad_addr],
5864 );
5865
5866 assert!(matches!(
5867 book.import_static_record(record, NOW, &context()),
5868 Err(DiscoveryBookError::NonDialableDirectAddress { addr }) if addr == bad_addr
5869 ));
5870 assert!(book.is_empty());
5871 }
5872 }
5873
5874 #[test]
5875 fn discovery_book_inserts_static_bootstrap_candidate_without_gossiping_it() {
5876 let mut book = ZakuraDiscoveryBook::default();
5877 let node_id = secret_key().public();
5878 let loopback_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8233);
5879 let node_addr = NodeAddr::new(node_id).with_direct_addresses([loopback_addr]);
5880
5881 book.insert_static_candidate(node_addr, NOW)
5882 .expect("configured static loopback candidate imports");
5883
5884 let mut rng = StdRng::seed_from_u64(7);
5885 assert!(book.sample_peers(10, &[], &[], NOW, &mut rng).is_empty());
5886 assert_eq!(
5887 book.dial_candidates(
5888 10,
5889 &[],
5890 DialCandidateExclusions {
5891 connected_node_ids: &[],
5892 in_flight_node_ids: &[],
5893 },
5894 NOW,
5895 (
5896 DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE,
5897 DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX,
5898 ),
5899 &mut rng,
5900 ),
5901 vec![ZakuraDiscoveryDialCandidate {
5902 node_id,
5903 direct_addrs: vec![loopback_addr],
5904 is_static: true,
5905 }]
5906 );
5907 }
5908
5909 #[test]
5910 fn discovery_book_static_bootstrap_candidate_rejects_unusable_addresses() {
5911 let bad_addrs = [
5912 SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8233),
5913 SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8233),
5914 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), 8233),
5915 SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1)), 8233),
5916 SocketAddr::new(IpAddr::V4(Ipv4Addr::BROADCAST), 8233),
5917 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
5918 ];
5919
5920 for bad_addr in bad_addrs {
5921 let mut book = ZakuraDiscoveryBook::default();
5922 let node_addr = NodeAddr::new(secret_key().public()).with_direct_addresses([bad_addr]);
5923
5924 assert!(matches!(
5925 book.insert_static_candidate(node_addr, NOW),
5926 Err(DiscoveryBookError::NonDialableDirectAddress { addr }) if addr == bad_addr
5927 ));
5928 assert!(book.is_empty());
5929 }
5930 }
5931
5932 #[test]
5933 fn discovery_book_sequence_rules_keep_newest_record() {
5934 let mut book = ZakuraDiscoveryBook::default();
5935 let secret = secret_key();
5936 let newer = record_with_secret(&secret, 10, service(1));
5937 let older = record_with_secret(&secret, 9, service(2));
5938 let equal = record_with_secret(&secret, 10, service(3));
5939 let node_id = newer.body.node_id;
5940
5941 assert_eq!(
5942 book.import_record(newer.clone(), None, NOW, &context())
5943 .unwrap(),
5944 ImportOutcome::Added
5945 );
5946 assert_eq!(
5947 book.import_record(older, Some(secret_key().public()), NOW + 1, &context())
5948 .unwrap(),
5949 ImportOutcome::IgnoredOlder
5950 );
5951 assert_eq!(
5952 book.get(&node_id).unwrap().record().body.services,
5953 newer.body.services
5954 );
5955
5956 let equal_source = secret_key().public();
5957 assert_eq!(
5958 book.import_record(equal, Some(equal_source), NOW + 2, &context())
5959 .unwrap(),
5960 ImportOutcome::MetadataUpdated
5961 );
5962 let entry = book.get(&node_id).unwrap();
5963 assert_eq!(entry.record().body.services, newer.body.services);
5964 assert_eq!(entry.source(), Some(equal_source));
5965 assert_eq!(entry.last_seen(), NOW + 2);
5966
5967 let newest = record_with_secret(&secret, 11, service(4));
5968 assert_eq!(
5969 book.import_record(newest.clone(), None, NOW + 3, &context())
5970 .unwrap(),
5971 ImportOutcome::Updated
5972 );
5973 assert_eq!(book.get(&node_id).unwrap().record(), &newest);
5974 }
5975
5976 #[test]
5977 fn discovery_book_import_batch_enforces_per_response_cap() {
5978 let mut book = ZakuraDiscoveryBook::new(ZakuraDiscoveryBookLimits {
5979 max_imported_records_per_response: 2,
5980 ..ZakuraDiscoveryBookLimits::default()
5981 });
5982 let records = (1u8..=4)
5983 .map(|index| {
5984 signed_record_with(u64::from(index), service(index as usize), test_addr(index))
5985 })
5986 .collect::<Vec<_>>();
5987
5988 let outcome = book.import_records(records, None, NOW, &context());
5989
5990 assert_eq!(outcome.attempted, 2);
5991 assert_eq!(outcome.added, 2);
5992 assert_eq!(outcome.dropped_for_limit, 2);
5993 assert_eq!(book.discovered_len(), 2);
5994 }
5995
5996 #[test]
5997 fn discovery_book_total_capacity_and_eviction_order_are_enforced() {
5998 let mut book = small_book(2);
5999
6000 let expired_secret = secret_key();
6001 let mut expired_body = body(&expired_secret);
6002 expired_body.sequence = 1;
6003 expired_body.expires_at_unix_secs = NOW - 1;
6004 let expired_context = DiscoveryRecordValidationContext {
6005 current_unix_secs: NOW - context().clock_skew_tolerance.as_secs(),
6006 ..context()
6007 };
6008 let expired = ZakuraNodeRecord::sign(expired_body, &expired_secret).expect("record signs");
6009 let expired_id = expired.body.node_id;
6010 book.import_record(
6011 expired,
6012 None,
6013 NOW - context().clock_skew_tolerance.as_secs(),
6014 &expired_context,
6015 )
6016 .expect("expired-later record imports before eviction time");
6017
6018 let failed = signed_record_with(2, service(2), test_addr(2));
6019 let failed_id = failed.body.node_id;
6020 book.import_record(failed, None, NOW, &context()).unwrap();
6021 for _ in 0..HIGH_DIAL_FAILURE_COUNT {
6022 book.mark_dial_failure(&failed_id, NOW);
6023 }
6024
6025 let survivor = signed_record_with(3, service(3), test_addr(3));
6026 let survivor_id = survivor.body.node_id;
6027 book.import_record(survivor, None, NOW, &context()).unwrap();
6028
6029 assert!(book.get(&expired_id).is_none());
6030 assert!(book.get(&failed_id).is_some());
6031 assert!(book.get(&survivor_id).is_some());
6032
6033 let replacement = signed_record_with(4, service(4), test_addr(4));
6034 let replacement_id = replacement.body.node_id;
6035 book.import_record(replacement, None, NOW, &context())
6036 .unwrap();
6037
6038 assert!(book.get(&failed_id).is_none());
6039 assert!(book.get(&survivor_id).is_some());
6040 assert!(book.get(&replacement_id).is_some());
6041 assert_eq!(book.discovered_len(), 2);
6042 }
6043
6044 #[test]
6045 fn discovery_book_static_entries_survive_eviction_storm() {
6046 let mut book = small_book(1);
6047 let static_record = signed_record_with(1, service(1), test_addr(1));
6048 let static_id = static_record.body.node_id;
6049 book.import_static_record(static_record, NOW, &context())
6050 .expect("static record imports");
6051
6052 for index in 2..=10 {
6053 book.import_record(
6054 signed_record_with(index.into(), service(index as usize), test_addr(index)),
6055 Some(secret_key().public()),
6056 NOW,
6057 &context(),
6058 )
6059 .expect("flood record imports");
6060 }
6061
6062 assert!(book
6063 .get(&static_id)
6064 .expect("static entry remains")
6065 .is_static());
6066 assert_eq!(book.discovered_len(), 1);
6067 assert_eq!(book.len(), 2);
6068 }
6069
6070 #[test]
6071 fn discovery_book_evicts_least_recently_successful_entry() {
6072 let mut book = small_book(2);
6073 let older_success = signed_record_with(1, service(1), test_addr(1));
6074 let recent_success = signed_record_with(2, service(2), test_addr(2));
6075 let replacement = signed_record_with(3, service(3), test_addr(3));
6076 let older_success_id = older_success.body.node_id;
6077 let recent_success_id = recent_success.body.node_id;
6078 let replacement_id = replacement.body.node_id;
6079
6080 let persisted = [older_success, recent_success, replacement]
6081 .into_iter()
6082 .enumerate()
6083 .map(|(index, record)| ZakuraDiscoveryPersistedEntry {
6084 record,
6085 source: None,
6086 is_static: false,
6087 last_seen: NOW,
6088 last_dial_attempt: None,
6089 last_success: Some(
6090 NOW + u64::try_from(index).expect("small test index fits in u64") + 1,
6091 ),
6092 failure_count: 0,
6093 });
6094
6095 let outcome = book.import_persisted_entries(persisted, NOW, &context());
6096
6097 assert_eq!(outcome.added, 3);
6098 assert!(book.get(&older_success_id).is_none());
6099 assert!(book.get(&recent_success_id).is_some());
6100 assert!(book.get(&replacement_id).is_some());
6101 assert_eq!(book.discovered_len(), 2);
6102 }
6103
6104 #[test]
6105 fn discovery_book_samples_are_bounded_excluded_and_service_filtered() {
6106 let mut book = ZakuraDiscoveryBook::default();
6107 let wanted = service(1);
6108 let other = service(2);
6109 let matching_a = signed_record_with(1, wanted.clone(), test_addr(1));
6110 let matching_b = signed_record_with(2, wanted.clone(), test_addr(2));
6111 let matching_c = signed_record_with(3, wanted.clone(), test_addr(3));
6112 let non_matching = signed_record_with(4, other, test_addr(4));
6113 let excluded = matching_a.body.node_id;
6114 book.import_record(matching_a, None, NOW, &context())
6115 .unwrap();
6116 book.import_record(matching_b, None, NOW, &context())
6117 .unwrap();
6118 book.import_record(matching_c, None, NOW, &context())
6119 .unwrap();
6120 book.import_record(non_matching, None, NOW, &context())
6121 .unwrap();
6122
6123 let mut rng = StdRng::seed_from_u64(7);
6124 let sample =
6125 book.sample_peers(1, std::slice::from_ref(&wanted), &[excluded], NOW, &mut rng);
6126 let sample_ids: HashSet<_> = sample.iter().map(|record| record.body.node_id).collect();
6127
6128 assert_eq!(sample.len(), 1);
6129 assert!(!sample_ids.contains(&excluded));
6130 assert!(sample
6131 .iter()
6132 .all(|record| record.body.services.contains(&wanted)));
6133 }
6134
6135 #[test]
6136 fn discovery_book_samples_skip_expired_records() {
6137 let mut book = ZakuraDiscoveryBook::default();
6138 let record = signed_record_with(1, service(1), test_addr(1));
6139 book.import_record(record, None, NOW, &context()).unwrap();
6140
6141 let mut rng = StdRng::seed_from_u64(7);
6142 let sample = book.sample_peers(10, &[], &[], NOW + 61, &mut rng);
6143
6144 assert!(sample.is_empty());
6145 }
6146
6147 #[test]
6148 fn discovery_book_dial_metadata_updates_candidates_without_blacklisting() {
6149 let mut book = ZakuraDiscoveryBook::default();
6150 let preferred = signed_record_with(1, service(1), test_addr(1));
6151 let failed = signed_record_with(2, service(1), test_addr(2));
6152 let failed_id = failed.body.node_id;
6153 let preferred_id = preferred.body.node_id;
6154
6155 import_confirmed_record(&mut book, failed.clone()).unwrap();
6156 import_confirmed_record(&mut book, preferred.clone()).unwrap();
6157 book.mark_dial_attempt(&failed_id, NOW);
6158 book.mark_dial_failure(&failed_id, NOW);
6159 book.mark_dial_success(&preferred_id, NOW + 1);
6160
6161 let mut rng = StdRng::seed_from_u64(7);
6162 let candidates = book.dial_candidates(
6163 10,
6164 &[service(1)],
6165 DialCandidateExclusions {
6166 connected_node_ids: &[],
6167 in_flight_node_ids: &[],
6168 },
6169 NOW + 1,
6170 (
6171 DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE,
6172 DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX,
6173 ),
6174 &mut rng,
6175 );
6176 assert_eq!(candidates, vec![candidate_for(&preferred, false)]);
6177 assert_eq!(book.get(&failed_id).unwrap().failure_count(), 1);
6178
6179 let candidates = book.dial_candidates(
6180 10,
6181 &[service(1)],
6182 DialCandidateExclusions {
6183 connected_node_ids: &[],
6184 in_flight_node_ids: &[],
6185 },
6186 NOW + 60,
6187 (
6188 DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE,
6189 DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX,
6190 ),
6191 &mut rng,
6192 );
6193 assert_eq!(
6194 candidates,
6195 vec![
6196 candidate_for(&preferred, false),
6197 candidate_for(&failed, false)
6198 ]
6199 );
6200 }
6201
6202 #[test]
6203 fn discovery_book_randomizes_comparable_non_static_dial_candidates() {
6204 let mut book = ZakuraDiscoveryBook::default();
6205 let records = (1u8..=32)
6206 .map(|index| signed_record_with(index.into(), service(1), test_addr(index)))
6207 .collect::<Vec<_>>();
6208
6209 for record in &records {
6210 import_confirmed_record(&mut book, record.clone()).expect("test record imports");
6211 }
6212
6213 let mut deterministic = records
6214 .iter()
6215 .map(|record| candidate_for(record, false))
6216 .collect::<Vec<_>>();
6217 deterministic.sort_by_key(|candidate| node_id_sort_key(&candidate.node_id));
6218
6219 let mut rng = StdRng::seed_from_u64(11);
6220 let sampled = book.dial_candidates(
6221 records.len(),
6222 &[service(1)],
6223 DialCandidateExclusions {
6224 connected_node_ids: &[],
6225 in_flight_node_ids: &[],
6226 },
6227 NOW,
6228 (
6229 DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE,
6230 DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX,
6231 ),
6232 &mut rng,
6233 );
6234 let mut other_rng = StdRng::seed_from_u64(12);
6235 let other_sampled = book.dial_candidates(
6236 records.len(),
6237 &[service(1)],
6238 DialCandidateExclusions {
6239 connected_node_ids: &[],
6240 in_flight_node_ids: &[],
6241 },
6242 NOW,
6243 (
6244 DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE,
6245 DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX,
6246 ),
6247 &mut other_rng,
6248 );
6249
6250 let sampled_ids: HashSet<_> = sampled.iter().map(|candidate| candidate.node_id).collect();
6251 assert_eq!(sampled_ids.len(), records.len());
6252 assert_eq!(
6253 sampled_ids,
6254 deterministic
6255 .iter()
6256 .map(|candidate| candidate.node_id)
6257 .collect::<HashSet<_>>()
6258 );
6259 assert_ne!(sampled, deterministic);
6260 assert_ne!(sampled, other_sampled);
6261 }
6262
6263 #[test]
6264 fn discovery_book_dial_backoff_uses_configured_bounds() {
6265 let mut book = ZakuraDiscoveryBook::default();
6266 let failed = signed_record_with(1, service(1), test_addr(1));
6267 let failed_id = failed.body.node_id;
6268
6269 import_confirmed_record(&mut book, failed.clone()).unwrap();
6270 book.mark_dial_attempt(&failed_id, NOW);
6271 book.mark_dial_failure(&failed_id, NOW);
6272
6273 assert!(book
6274 .dial_candidates(
6275 10,
6276 &[service(1)],
6277 DialCandidateExclusions {
6278 connected_node_ids: &[],
6279 in_flight_node_ids: &[],
6280 },
6281 NOW + 29,
6282 (Duration::from_secs(30), Duration::from_secs(300)),
6283 &mut StdRng::seed_from_u64(7),
6284 )
6285 .is_empty());
6286 let mut rng = StdRng::seed_from_u64(7);
6287 assert_eq!(
6288 book.dial_candidates(
6289 10,
6290 &[service(1)],
6291 DialCandidateExclusions {
6292 connected_node_ids: &[],
6293 in_flight_node_ids: &[],
6294 },
6295 NOW + 30,
6296 (Duration::from_secs(30), Duration::from_secs(300)),
6297 &mut rng,
6298 ),
6299 vec![candidate_for(&failed, false)]
6300 );
6301 }
6302
6303 #[test]
6304 fn discovery_book_short_lived_exchange_backoff_is_non_punitive() {
6305 let mut book = ZakuraDiscoveryBook::default();
6306 let exchanged = signed_record_with(1, service(1), test_addr(1));
6307 let exchanged_id = exchanged.body.node_id;
6308
6309 import_confirmed_record(&mut book, exchanged.clone()).unwrap();
6310 book.mark_dial_success(&exchanged_id, NOW);
6311 book.mark_short_lived_exchange(&exchanged_id, NOW);
6312
6313 let entry = book.get(&exchanged_id).unwrap();
6314 assert_eq!(entry.failure_count(), 0);
6315 assert_eq!(entry.last_short_lived_exchange(), Some(NOW));
6316
6317 assert!(book
6318 .dial_candidates(
6319 10,
6320 &[service(1)],
6321 DialCandidateExclusions {
6322 connected_node_ids: &[],
6323 in_flight_node_ids: &[],
6324 },
6325 NOW + 29,
6326 (Duration::from_secs(30), Duration::from_secs(300)),
6327 &mut StdRng::seed_from_u64(7),
6328 )
6329 .is_empty());
6330 assert_eq!(
6331 book.dial_candidates(
6332 10,
6333 &[service(1)],
6334 DialCandidateExclusions {
6335 connected_node_ids: &[],
6336 in_flight_node_ids: &[],
6337 },
6338 NOW + 30,
6339 (Duration::from_secs(30), Duration::from_secs(300)),
6340 &mut StdRng::seed_from_u64(7),
6341 ),
6342 vec![candidate_for(&exchanged, false)]
6343 );
6344 }
6345
6346 #[test]
6347 fn discovery_book_services_and_persistence_hooks_revalidate_records() {
6348 let mut book = ZakuraDiscoveryBook::default();
6349 let record = signed_record_with(1, service(9), test_addr(9));
6350 let node_id = record.body.node_id;
6351 import_confirmed_record(&mut book, record.clone()).unwrap();
6352 book.mark_dial_attempt(&node_id, NOW + 1);
6353 book.mark_dial_failure(&node_id, NOW + 1);
6354
6355 assert_eq!(book.services_for(&node_id), Some(vec![service(9)]));
6356
6357 let mut persisted = book.persisted_entries();
6358 persisted.push(ZakuraDiscoveryPersistedEntry {
6359 record: {
6360 let mut invalid = record.clone();
6361 invalid.body.sequence += 1;
6362 invalid
6363 },
6364 source: None,
6365 is_static: false,
6366 last_seen: NOW,
6367 last_dial_attempt: None,
6368 last_success: None,
6369 failure_count: 0,
6370 });
6371
6372 let mut reloaded = ZakuraDiscoveryBook::default();
6373 let outcome = reloaded.import_persisted_entries(persisted, NOW, &context());
6374
6375 assert_eq!(outcome.added, 1);
6376 assert_eq!(outcome.rejected, 1);
6377 let reloaded_entry = reloaded
6378 .get(&node_id)
6379 .expect("valid persisted entry reloads");
6380 assert_eq!(reloaded_entry.record(), &record);
6381 assert_eq!(reloaded_entry.last_dial_attempt(), Some(NOW + 1));
6382 assert_eq!(reloaded_entry.failure_count(), 1);
6383 }
6384
6385 #[test]
6386 fn self_record_includes_local_node_addresses_services_and_verifies() {
6387 let secret = secret_key();
6388 let node_id = secret.public();
6389 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
6390 let handle = discovery_handle_at(
6391 local_config_with(
6392 secret,
6393 vec![test_addr(42)],
6394 vec![
6395 ZakuraServiceId::legacy_requests(),
6396 ZakuraServiceId::discovery(),
6397 ],
6398 ),
6399 ZakuraDiscoveryConfig::default(),
6400 connected_rx,
6401 NOW,
6402 );
6403
6404 let record = handle.current_self_record();
6405
6406 assert_eq!(record.body.node_id, node_id);
6407 assert_eq!(record.body.direct_addrs, vec![test_addr(42)]);
6408 assert_eq!(
6409 record.body.services,
6410 vec![
6411 ZakuraServiceId::discovery(),
6412 ZakuraServiceId::legacy_requests()
6413 ]
6414 );
6415 record.verify(&context()).expect("self-record verifies");
6416 }
6417
6418 #[tokio::test]
6419 async fn self_record_sequence_increases_when_services_change() {
6420 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
6421 let handle = discovery_handle_at(
6422 local_config_with(secret_key(), vec![test_addr(43)], vec![service(1)]),
6423 ZakuraDiscoveryConfig::default(),
6424 connected_rx,
6425 NOW,
6426 );
6427 let initial = handle.current_self_record();
6428
6429 let updated = handle
6430 .update_advertised_services(vec![service(2)])
6431 .await
6432 .expect("updated self-record signs");
6433
6434 assert!(updated.body.sequence > initial.body.sequence);
6435 assert_eq!(updated.body.services, vec![service(2)]);
6436 updated
6437 .verify(&DiscoveryRecordValidationContext {
6438 current_unix_secs: current_unix_secs(),
6439 ..context()
6440 })
6441 .expect("updated record verifies");
6442 }
6443
6444 #[test]
6445 fn derived_sequence_does_not_regress_after_restart() {
6446 let secret = secret_key();
6447 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
6448 let first = discovery_handle_at(
6449 local_config_with(secret.clone(), vec![test_addr(44)], vec![service(1)]),
6450 ZakuraDiscoveryConfig::default(),
6451 connected_rx,
6452 NOW,
6453 );
6454 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
6455 let restarted = discovery_handle_at(
6456 local_config_with(secret, vec![test_addr(44)], vec![service(1)]),
6457 ZakuraDiscoveryConfig::default(),
6458 connected_rx,
6459 NOW + 1,
6460 );
6461
6462 assert!(
6463 restarted.current_self_record().body.sequence
6464 > first.current_self_record().body.sequence
6465 );
6466 assert_eq!(
6467 restarted.current_self_record().body.node_id,
6468 first.current_self_record().body.node_id
6469 );
6470 }
6471
6472 #[test]
6473 fn persisted_sequence_seed_makes_fast_restart_record_newer() {
6474 let secret = secret_key();
6475 let same_unix_second = NOW;
6476 let same_sequence_tick = 10_000_000_000;
6477 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
6478 let first = discovery_handle_at_with_sequence(
6479 local_config_with(secret.clone(), vec![test_addr(44)], vec![service(1)]),
6480 ZakuraDiscoveryConfig::default(),
6481 connected_rx,
6482 same_unix_second,
6483 same_sequence_tick,
6484 );
6485 let first_record = first.current_self_record();
6486
6487 let mut restarted_config = local_config_with(secret, vec![test_addr(45)], vec![service(2)]);
6488 restarted_config.last_authored_sequence = Some(first_record.body.sequence);
6489 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
6490 let restarted = discovery_handle_at_with_sequence(
6491 restarted_config,
6492 ZakuraDiscoveryConfig::default(),
6493 connected_rx,
6494 same_unix_second,
6495 same_sequence_tick,
6496 );
6497 let restarted_record = restarted.current_self_record();
6498
6499 assert_eq!(restarted_record.body.node_id, first_record.body.node_id);
6500 assert!(restarted_record.body.sequence > first_record.body.sequence);
6501 assert_eq!(restarted_record.body.direct_addrs, vec![test_addr(45)]);
6502 assert_eq!(restarted_record.body.services, vec![service(2)]);
6503 }
6504
6505 #[test]
6506 fn self_record_filters_non_routable_direct_addresses() {
6507 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
6508 let handle = discovery_handle_at(
6509 local_config_with(
6510 secret_key(),
6511 vec![
6512 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8233),
6513 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8233),
6514 SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8233),
6515 SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8233),
6516 test_addr(45),
6517 ],
6518 vec![service(1)],
6519 ),
6520 ZakuraDiscoveryConfig::default(),
6521 connected_rx,
6522 NOW,
6523 );
6524
6525 assert_eq!(
6526 handle.current_self_record().body.direct_addrs,
6527 vec![test_addr(45)]
6528 );
6529 }
6530
6531 #[tokio::test]
6532 async fn active_services_follow_supervisor_watch_connect_and_disconnect() {
6533 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6534 let handle = discovery_handle_with_connected(connected_rx);
6535 let record = runtime_record_with(1, service(9), test_addr(9));
6536 let node_id = record.body.node_id;
6537 connected_tx.send_replace(vec![peer_id_for(node_id)]);
6538 handle
6539 .import_connected_peer_record(record, node_id)
6540 .await
6541 .expect("connected self-record imports");
6542
6543 assert_eq!(
6544 handle.active_services(node_id).await,
6545 Some(vec![service(9)])
6546 );
6547
6548 connected_tx.send_replace(Vec::new());
6549 assert_eq!(handle.active_services(node_id).await, None);
6550 }
6551
6552 #[tokio::test]
6553 async fn active_services_drop_connected_hello_absent_from_supervisor_watch() {
6554 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
6555 let handle = discovery_handle_with_connected(connected_rx);
6556 let record = runtime_record_with(1, service(9), test_addr(9));
6557 let node_id = record.body.node_id;
6558 handle
6559 .import_connected_peer_record(record, node_id)
6560 .await
6561 .expect("connected self-record imports");
6562
6563 assert!(handle
6564 .service_candidates(&service(9), false, &[])
6565 .await
6566 .connected
6567 .is_empty());
6568 assert_eq!(handle.active_services(node_id).await, None);
6569 assert!(handle.inner.lock().await.active_services.is_empty());
6570 }
6571
6572 #[tokio::test]
6573 async fn gossiped_service_record_does_not_create_active_services() {
6574 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6575 let handle = discovery_handle_with_connected(connected_rx);
6576 let record = runtime_record_with(1, service(9), test_addr(9));
6577 let node_id = record.body.node_id;
6578
6579 handle.import_peer_records([record], None).await;
6580 connected_tx.send_replace(vec![peer_id_for(node_id)]);
6581
6582 assert_eq!(handle.active_services(node_id).await, None);
6583 }
6584
6585 #[tokio::test]
6586 async fn connected_services_import_creates_and_updates_first_party_live_summary() {
6587 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6588 let handle = discovery_handle_with_connected(connected_rx);
6589 let node_id = secret_key().public();
6590 let now = current_unix_secs();
6591 connected_tx.send_replace(vec![peer_id_for(node_id)]);
6592
6593 let first_summary = DiscoveryServiceSummary {
6594 peer_exchange_slots_free: 1,
6595 ..discovery_summary()
6596 };
6597 handle
6598 .import_connected_peer_services_at(
6599 first_party_services(
6600 node_id,
6601 u64::MAX,
6602 vec![ServiceSummaryEnvelope::discovery(&first_summary)
6603 .expect("test discovery summary encodes")],
6604 ),
6605 node_id,
6606 now,
6607 )
6608 .await
6609 .expect("first-party services import");
6610
6611 assert_eq!(
6612 handle.active_services(node_id).await,
6613 Some(vec![ZakuraServiceId::discovery()])
6614 );
6615 let cached = handle
6616 .live_service_summaries_at(node_id, now)
6617 .await
6618 .expect("live summary entry exists");
6619 assert_eq!(cached.len(), 1);
6620 assert_eq!(cached[0].observed_at_unix_secs, now);
6621 assert_eq!(
6622 cached[0].expires_at_unix_secs,
6623 now + DEFAULT_LIVE_SERVICE_SUMMARY_TTL.as_secs()
6624 );
6625 assert_eq!(
6626 cached[0].summary,
6627 ZakuraLiveServiceSummary::Discovery(first_summary)
6628 );
6629
6630 let updated_summary = DiscoveryServiceSummary {
6631 peer_exchange_slots_free: 9,
6632 ..discovery_summary()
6633 };
6634 handle
6635 .import_connected_peer_services_at(
6636 first_party_services(
6637 node_id,
6638 now + 10,
6639 vec![ServiceSummaryEnvelope::discovery(&updated_summary)
6640 .expect("test discovery summary encodes")],
6641 ),
6642 node_id,
6643 now + 1,
6644 )
6645 .await
6646 .expect("newer first-party services update");
6647
6648 let cached = handle
6649 .live_service_summaries_at(node_id, now + 1)
6650 .await
6651 .expect("updated live summary entry exists");
6652 assert_eq!(cached.len(), 1);
6653 assert_eq!(cached[0].observed_at_unix_secs, now + 1);
6654 assert_eq!(cached[0].expires_at_unix_secs, now + 10);
6655 assert_eq!(
6656 cached[0].summary,
6657 ZakuraLiveServiceSummary::Discovery(updated_summary)
6658 );
6659 }
6660
6661 #[tokio::test]
6662 async fn connected_services_mismatched_node_id_rejected_without_live_summary() {
6663 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6664 let handle = discovery_handle_with_connected(connected_rx);
6665 let peer_node_id = secret_key().public();
6666 let claimed_node_id = secret_key().public();
6667 connected_tx.send_replace(vec![peer_id_for(peer_node_id)]);
6668
6669 let error = handle
6670 .import_connected_peer_services_at(
6671 first_party_services(
6672 claimed_node_id,
6673 NOW + 30,
6674 vec![ServiceSummaryEnvelope::discovery(&discovery_summary())
6675 .expect("test discovery summary encodes")],
6676 ),
6677 peer_node_id,
6678 NOW,
6679 )
6680 .await
6681 .expect_err("mismatched services are rejected");
6682
6683 assert!(matches!(
6684 error,
6685 DiscoveryWireError::MismatchedNodeId {
6686 field: "services node id"
6687 }
6688 ));
6689 assert_eq!(
6690 handle.live_service_summaries_at(peer_node_id, NOW).await,
6691 None
6692 );
6693 assert_eq!(
6694 handle.live_service_summaries_at(claimed_node_id, NOW).await,
6695 None
6696 );
6697 }
6698
6699 #[tokio::test]
6700 async fn peers_response_and_gossiped_record_do_not_create_live_summary() {
6701 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6702 let handle = discovery_handle_with_connected(connected_rx);
6703 let record = runtime_record_with(1, service(9), test_addr(9));
6704 let node_id = record.body.node_id;
6705 let message = DiscoveryMessage::Peers {
6706 records: vec![record],
6707 };
6708 let encoded = message.encode().expect("test peers response encodes");
6709 let DiscoveryMessage::Peers { records } =
6710 DiscoveryMessage::decode(&encoded).expect("test peers response decodes")
6711 else {
6712 panic!("PEERS response decoded as a different message");
6713 };
6714
6715 handle.import_peer_records(records, None).await;
6716 connected_tx.send_replace(vec![peer_id_for(node_id)]);
6717
6718 assert_eq!(handle.live_service_summaries_at(node_id, NOW).await, None);
6719 assert_eq!(handle.active_services(node_id).await, None);
6720 assert!(handle.record_for(node_id).await.is_some());
6721 }
6722
6723 #[tokio::test]
6724 async fn active_services_prefer_latest_connected_hello_over_book_record() {
6725 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6726 let handle = discovery_handle_with_connected(connected_rx);
6727 let secret = secret_key();
6728 let active_record = runtime_record_with_secret(&secret, 1, service(1), test_addr(1));
6729 let stale_book_record = runtime_record_with_secret(&secret, 2, service(2), test_addr(2));
6730 let node_id = active_record.body.node_id;
6731
6732 handle
6733 .import_connected_peer_record(active_record, node_id)
6734 .await
6735 .expect("connected self-record imports");
6736 handle
6737 .import_peer_record(stale_book_record, Some(secret_key().public()))
6738 .await
6739 .expect("newer third-party record imports into book");
6740 connected_tx.send_replace(vec![peer_id_for(node_id)]);
6741
6742 assert_eq!(
6743 handle.active_services(node_id).await,
6744 Some(vec![service(1)])
6745 );
6746 assert_eq!(
6747 handle
6748 .service_candidates(&service(1), false, &[])
6749 .await
6750 .connected,
6751 vec![node_id]
6752 );
6753 }
6754
6755 #[tokio::test]
6756 async fn active_services_ignore_stale_and_equal_sequence_conflicting_hellos() {
6757 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6758 let handle = discovery_handle_with_connected(connected_rx);
6759 let secret = secret_key();
6760 let newer = runtime_record_with_secret(&secret, 2, service(2), test_addr(2));
6761 let older = runtime_record_with_secret(&secret, 1, service(1), test_addr(1));
6762 let equal_conflicting = runtime_record_with_secret(&secret, 2, service(3), test_addr(3));
6763 let stored_newer = runtime_record_with_secret(&secret, 4, service(4), test_addr(4));
6764 let older_than_stored = runtime_record_with_secret(&secret, 3, service(3), test_addr(3));
6765 let node_id = newer.body.node_id;
6766 connected_tx.send_replace(vec![peer_id_for(node_id)]);
6767
6768 handle
6769 .import_connected_peer_record(newer, node_id)
6770 .await
6771 .expect("newer connected self-record imports");
6772 assert_eq!(
6773 handle
6774 .import_connected_peer_record(older, node_id)
6775 .await
6776 .expect("older connected self-record verifies and is ignored"),
6777 ImportOutcome::IgnoredOlder
6778 );
6779 assert_eq!(
6780 handle
6781 .import_connected_peer_record(equal_conflicting, node_id)
6782 .await
6783 .expect("equal-sequence connected self-record refreshes metadata only"),
6784 ImportOutcome::MetadataUpdated
6785 );
6786 handle
6787 .import_peer_record(stored_newer, Some(secret_key().public()))
6788 .await
6789 .expect("newer third-party record imports into book");
6790 assert_eq!(
6791 handle
6792 .import_connected_peer_record(older_than_stored, node_id)
6793 .await
6794 .expect("connected self-record older than the book is ignored"),
6795 ImportOutcome::IgnoredOlder
6796 );
6797
6798 assert_eq!(
6799 handle.active_services(node_id).await,
6800 Some(vec![service(2)])
6801 );
6802 assert_eq!(
6803 handle
6804 .service_candidates(&service(2), false, &[])
6805 .await
6806 .connected,
6807 vec![node_id]
6808 );
6809 assert!(handle
6810 .service_candidates(&service(3), false, &[])
6811 .await
6812 .connected
6813 .is_empty());
6814 }
6815
6816 #[tokio::test]
6817 async fn addressless_active_services_ignore_stale_and_equal_sequence_conflicting_hellos() {
6818 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6819 let handle = discovery_handle_with_connected(connected_rx);
6820 let secret = secret_key();
6821 let newer = runtime_record_with_secret_and_addrs(&secret, 2, service(2), Vec::new());
6822 let older = runtime_record_with_secret_and_addrs(&secret, 1, service(1), Vec::new());
6823 let equal_conflicting =
6824 runtime_record_with_secret_and_addrs(&secret, 2, service(3), Vec::new());
6825 let node_id = newer.body.node_id;
6826 connected_tx.send_replace(vec![peer_id_for(node_id)]);
6827
6828 assert!(matches!(
6829 handle.import_connected_peer_record(newer, node_id).await,
6830 Err(DiscoveryBookError::NoUsableDirectAddress)
6831 ));
6832 assert!(matches!(
6833 handle.import_connected_peer_record(older, node_id).await,
6834 Err(DiscoveryBookError::NoUsableDirectAddress)
6835 ));
6836 assert!(matches!(
6837 handle
6838 .import_connected_peer_record(equal_conflicting, node_id)
6839 .await,
6840 Err(DiscoveryBookError::NoUsableDirectAddress)
6841 ));
6842
6843 assert_eq!(
6844 handle.active_services(node_id).await,
6845 Some(vec![service(2)])
6846 );
6847 assert_eq!(
6848 handle
6849 .service_candidates(&service(2), false, &[])
6850 .await
6851 .connected,
6852 vec![node_id]
6853 );
6854 assert!(handle
6855 .service_candidates(&service(3), false, &[])
6856 .await
6857 .connected
6858 .is_empty());
6859 }
6860
6861 #[tokio::test]
6862 async fn expired_connected_hello_does_not_create_active_services() {
6863 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6864 let handle = discovery_handle_with_connected(connected_rx);
6865 let secret = secret_key();
6866 let mut body = body(&secret);
6867 body.sequence = 1;
6868 body.direct_addrs = vec![test_addr(1)];
6869 body.services = vec![service(1)];
6870 body.expires_at_unix_secs = current_unix_secs()
6871 .saturating_sub(DEFAULT_DISCOVERY_CLOCK_SKEW_TOLERANCE.as_secs())
6872 .saturating_sub(1);
6873 let expired = ZakuraNodeRecord::sign(body, &secret).expect("expired record signs");
6874 let node_id = expired.body.node_id;
6875 connected_tx.send_replace(vec![peer_id_for(node_id)]);
6876
6877 assert!(matches!(
6878 handle.import_connected_peer_record(expired, node_id).await,
6879 Err(DiscoveryBookError::Record(DiscoveryRecordError::Expired))
6880 ));
6881 assert_eq!(handle.active_services(node_id).await, None);
6882 assert!(handle
6883 .service_candidates(&service(1), false, &[])
6884 .await
6885 .connected
6886 .is_empty());
6887 }
6888
6889 #[tokio::test]
6890 async fn service_candidates_require_explicit_general_fallback() {
6891 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6892 let handle = discovery_handle_at(
6893 local_config_with(secret_key(), vec![test_addr(49)], vec![service(1)]),
6894 ZakuraDiscoveryConfig {
6895 max_zakura_connections: 8,
6896 discovery_connection_headroom: 1,
6897 ..ZakuraDiscoveryConfig::default()
6898 },
6899 connected_rx,
6900 NOW,
6901 );
6902 let active = runtime_record_with(1, service(1), test_addr(1));
6903 let active_id = active.body.node_id;
6904 let discovered = runtime_record_with(2, service(1), test_addr(2));
6905 let general = runtime_record_with(3, service(3), test_addr(3));
6906 handle
6907 .import_connected_peer_record(active, active_id)
6908 .await
6909 .expect("connected self-record imports");
6910 handle
6911 .import_peer_record(discovered.clone(), Some(discovered.body.node_id))
6912 .await
6913 .expect("discovered first-party record imports");
6914 handle
6915 .import_peer_record(general.clone(), Some(general.body.node_id))
6916 .await
6917 .expect("general first-party record imports");
6918 connected_tx.send_replace(vec![peer_id_for(active_id)]);
6919
6920 let matching = handle.service_candidates(&service(1), true, &[]).await;
6921 assert_eq!(matching.connected, vec![active_id]);
6922 assert_eq!(matching.discovered, vec![candidate_for(&discovered, false)]);
6923 assert!(!matching.used_fallback);
6924
6925 assert_eq!(
6926 handle
6927 .service_candidates(&service(2), false, &[])
6928 .await
6929 .discovered,
6930 Vec::<ZakuraDiscoveryDialCandidate>::new()
6931 );
6932
6933 let fallback = handle
6934 .service_candidates(&service(2), true, &[discovered.body.node_id])
6935 .await;
6936 assert!(fallback.connected.is_empty());
6937 assert_eq!(fallback.discovered, vec![candidate_for(&general, false)]);
6938 assert!(fallback.used_fallback);
6939 }
6940
6941 #[tokio::test]
6942 async fn live_first_party_summary_prioritizes_service_candidates_until_stale() {
6943 let (connected_tx, connected_rx) = watch::channel(Vec::new());
6944 let handle = discovery_handle_with_connected(connected_rx);
6945 let now = current_unix_secs();
6946 let first = runtime_record_with(1, ZakuraServiceId::header_sync(), test_addr(1));
6947 let second = runtime_record_with(2, ZakuraServiceId::header_sync(), test_addr(2));
6948 let first_id = first.body.node_id;
6949 let second_id = second.body.node_id;
6950 connected_tx.send_replace(vec![peer_id_for(first_id), peer_id_for(second_id)]);
6951
6952 handle
6953 .import_connected_peer_record(first, first_id)
6954 .await
6955 .expect("first connected self-record imports");
6956 handle
6957 .import_connected_peer_record(second, second_id)
6958 .await
6959 .expect("second connected self-record imports");
6960
6961 let initial = handle
6962 .service_candidates(&ZakuraServiceId::header_sync(), false, &[])
6963 .await
6964 .connected;
6965 assert_eq!(initial.len(), 2);
6966 let preferred_id = initial[1];
6967 let mut preferred_summary = header_summary();
6968 preferred_summary.inbound_slots_free = 8;
6969 preferred_summary.outbound_slots_free = 8;
6970
6971 handle
6972 .import_connected_peer_services_at(
6973 first_party_services(
6974 preferred_id,
6975 now + 30,
6976 vec![ServiceSummaryEnvelope::header_sync(&preferred_summary)
6977 .expect("test header summary encodes")],
6978 ),
6979 preferred_id,
6980 now,
6981 )
6982 .await
6983 .expect("first-party header summary imports");
6984
6985 let with_live_summary = handle
6986 .service_candidates(&ZakuraServiceId::header_sync(), false, &[])
6987 .await
6988 .connected;
6989 assert_eq!(with_live_summary[0], preferred_id);
6990
6991 handle
6992 .import_connected_peer_services_at(
6993 first_party_services(
6994 preferred_id,
6995 now,
6996 vec![ServiceSummaryEnvelope::header_sync(&preferred_summary)
6997 .expect("test header summary encodes")],
6998 ),
6999 preferred_id,
7000 now,
7001 )
7002 .await
7003 .expect("expired first-party header summary is dropped");
7004
7005 assert_eq!(
7006 handle
7007 .service_candidates(&ZakuraServiceId::header_sync(), false, &[])
7008 .await
7009 .connected,
7010 initial
7011 );
7012 }
7013
7014 #[tokio::test]
7015 async fn header_sync_candidates_prefer_useful_first_party_height() {
7016 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7017 let handle = discovery_handle_with_connected(connected_rx);
7018 let now = current_unix_secs();
7019 let without_summary = runtime_record_with(1, ZakuraServiceId::header_sync(), test_addr(1));
7020 let with_summary = runtime_record_with(2, ZakuraServiceId::header_sync(), test_addr(2));
7021 let without_summary_id = without_summary.body.node_id;
7022 let with_summary_id = with_summary.body.node_id;
7023 connected_tx.send_replace(vec![
7024 peer_id_for(without_summary_id),
7025 peer_id_for(with_summary_id),
7026 ]);
7027
7028 handle
7029 .import_connected_peer_record(without_summary, without_summary_id)
7030 .await
7031 .expect("connected record imports");
7032 handle
7033 .import_connected_peer_record(with_summary, with_summary_id)
7034 .await
7035 .expect("connected record imports");
7036
7037 let mut summary = header_summary();
7038 summary.best_height = block::Height(20);
7039 summary.inbound_slots_free = 1;
7040 handle
7041 .import_connected_peer_services_at(
7042 first_party_services(
7043 with_summary_id,
7044 now + 30,
7045 vec![ServiceSummaryEnvelope::header_sync(&summary)
7046 .expect("test header summary encodes")],
7047 ),
7048 with_summary_id,
7049 now,
7050 )
7051 .await
7052 .expect("first-party header summary imports");
7053
7054 let candidates = handle
7055 .header_sync_candidates(
7056 &ZakuraHeaderSyncCandidateState {
7057 target_height: block::Height(10),
7058 ..ZakuraHeaderSyncCandidateState::default()
7059 },
7060 false,
7061 &[],
7062 )
7063 .await;
7064
7065 assert_eq!(candidates.connected[0], with_summary_id);
7066 assert!(candidates.connected.contains(&without_summary_id));
7067 }
7068
7069 #[tokio::test]
7070 async fn retired_header_summary_scores_only_dual_stack_header_sync_candidates() {
7071 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7072 let handle = discovery_handle_with_connected(connected_rx);
7073 let now = current_unix_secs();
7074 let v1_only = runtime_record_with(1, ZakuraServiceId::header_sync_retired(), test_addr(1));
7075 let dual_stack = runtime_record_with_services(
7076 2,
7077 vec![
7078 ZakuraServiceId::header_sync_retired(),
7079 ZakuraServiceId::header_sync(),
7080 ],
7081 test_addr(2),
7082 );
7083 let current_only = runtime_record_with(3, ZakuraServiceId::header_sync(), test_addr(3));
7084 let v1_only_id = v1_only.body.node_id;
7085 let dual_stack_id = dual_stack.body.node_id;
7086 let current_only_id = current_only.body.node_id;
7087 connected_tx.send_replace(vec![
7088 peer_id_for(v1_only_id),
7089 peer_id_for(dual_stack_id),
7090 peer_id_for(current_only_id),
7091 ]);
7092
7093 for record in [v1_only, dual_stack, current_only] {
7094 let node_id = record.body.node_id;
7095 handle
7096 .import_connected_peer_record(record, node_id)
7097 .await
7098 .expect("connected record imports");
7099 }
7100
7101 let mut summary = header_summary();
7102 summary.best_height = block::Height(20);
7103 summary.inbound_slots_free = 8;
7104 for node_id in [v1_only_id, dual_stack_id] {
7105 handle
7106 .import_connected_peer_services_at(
7107 first_party_services(
7108 node_id,
7109 now + 30,
7110 vec![retired_header_sync_envelope(&summary)],
7111 ),
7112 node_id,
7113 now,
7114 )
7115 .await
7116 .expect("retired first-party header summary imports");
7117 }
7118
7119 let generic = handle
7120 .service_candidates(&ZakuraServiceId::header_sync(), false, &[])
7121 .await;
7122 assert_eq!(generic.connected[0], dual_stack_id);
7123 assert!(generic.connected.contains(¤t_only_id));
7124 assert!(!generic.connected.contains(&v1_only_id));
7125
7126 let candidates = handle
7127 .header_sync_candidates(
7128 &ZakuraHeaderSyncCandidateState {
7129 target_height: block::Height(10),
7130 ..ZakuraHeaderSyncCandidateState::default()
7131 },
7132 false,
7133 &[],
7134 )
7135 .await;
7136
7137 assert_eq!(candidates.connected[0], dual_stack_id);
7138 assert!(candidates.connected.contains(¤t_only_id));
7139 assert!(!candidates.connected.contains(&v1_only_id));
7140 }
7141
7142 #[tokio::test]
7143 async fn header_sync_candidates_skip_full_inbound_unless_already_admitted() {
7144 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7145 let handle = discovery_handle_with_connected(connected_rx);
7146 let now = current_unix_secs();
7147 let full = runtime_record_with(1, ZakuraServiceId::header_sync(), test_addr(1));
7148 let open = runtime_record_with(2, ZakuraServiceId::header_sync(), test_addr(2));
7149 let full_id = full.body.node_id;
7150 let open_id = open.body.node_id;
7151 connected_tx.send_replace(vec![peer_id_for(full_id), peer_id_for(open_id)]);
7152
7153 handle
7154 .import_connected_peer_record(full, full_id)
7155 .await
7156 .expect("connected record imports");
7157 handle
7158 .import_connected_peer_record(open, open_id)
7159 .await
7160 .expect("connected record imports");
7161
7162 let mut full_summary = header_summary();
7163 full_summary.best_height = block::Height(20);
7164 full_summary.inbound_slots_free = 0;
7165 let mut open_summary = full_summary;
7166 open_summary.inbound_slots_free = 2;
7167 for (node_id, summary) in [(full_id, full_summary), (open_id, open_summary)] {
7168 handle
7169 .import_connected_peer_services_at(
7170 first_party_services(
7171 node_id,
7172 now + 30,
7173 vec![ServiceSummaryEnvelope::header_sync(&summary)
7174 .expect("test header summary encodes")],
7175 ),
7176 node_id,
7177 now,
7178 )
7179 .await
7180 .expect("first-party header summary imports");
7181 }
7182
7183 let candidates = handle
7184 .header_sync_candidates(
7185 &ZakuraHeaderSyncCandidateState {
7186 target_height: block::Height(10),
7187 ..ZakuraHeaderSyncCandidateState::default()
7188 },
7189 false,
7190 &[],
7191 )
7192 .await;
7193 assert_eq!(candidates.connected, vec![open_id]);
7194
7195 let candidates = handle
7196 .header_sync_candidates(
7197 &ZakuraHeaderSyncCandidateState {
7198 target_height: block::Height(10),
7199 admitted_node_ids: vec![full_id],
7200 ..ZakuraHeaderSyncCandidateState::default()
7201 },
7202 false,
7203 &[],
7204 )
7205 .await;
7206 assert!(candidates.connected.contains(&full_id));
7207 assert!(candidates.connected.contains(&open_id));
7208 }
7209
7210 #[tokio::test]
7211 async fn block_sync_service_candidates_prefer_covering_free_slot_summary() {
7212 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7213 let handle = discovery_handle_with_connected(connected_rx);
7214 let now = current_unix_secs();
7215 let no_summary = runtime_record_with(1, ZakuraServiceId::block_sync(), test_addr(1));
7216 let covering = runtime_record_with(2, ZakuraServiceId::block_sync(), test_addr(2));
7217 let prefix = runtime_record_with(3, ZakuraServiceId::block_sync(), test_addr(3));
7218 let full = runtime_record_with(4, ZakuraServiceId::block_sync(), test_addr(4));
7219 let not_block_sync = runtime_record_with(5, ZakuraServiceId::header_sync(), test_addr(5));
7220 let no_summary_id = no_summary.body.node_id;
7221 let covering_id = covering.body.node_id;
7222 let prefix_id = prefix.body.node_id;
7223 let full_id = full.body.node_id;
7224 let not_block_sync_id = not_block_sync.body.node_id;
7225 connected_tx.send_replace(vec![
7226 peer_id_for(no_summary_id),
7227 peer_id_for(covering_id),
7228 peer_id_for(prefix_id),
7229 peer_id_for(full_id),
7230 peer_id_for(not_block_sync_id),
7231 ]);
7232
7233 for record in [no_summary, covering, prefix, full, not_block_sync] {
7234 let node_id = record.body.node_id;
7235 handle
7236 .import_connected_peer_record(record, node_id)
7237 .await
7238 .expect("connected record imports");
7239 }
7240
7241 let mut covering_summary = block_sync_summary();
7242 covering_summary.servable_low = block::Height(10);
7243 covering_summary.servable_high = block::Height(20);
7244 covering_summary.free_slots = 2;
7245 let mut prefix_summary = covering_summary;
7246 prefix_summary.servable_high = block::Height(12);
7247 prefix_summary.free_slots = 4;
7248 let mut full_summary = covering_summary;
7249 full_summary.free_slots = 0;
7250 for (node_id, summary) in [
7251 (covering_id, covering_summary),
7252 (prefix_id, prefix_summary),
7253 (full_id, full_summary),
7254 ] {
7255 handle
7256 .import_connected_peer_services_at(
7257 first_party_services(
7258 node_id,
7259 now + 30,
7260 vec![ServiceSummaryEnvelope::block_sync(&summary)
7261 .expect("test block summary encodes")],
7262 ),
7263 node_id,
7264 now,
7265 )
7266 .await
7267 .expect("first-party block summary imports");
7268 }
7269
7270 let generic = handle
7271 .service_candidates(&ZakuraServiceId::block_sync(), false, &[])
7272 .await;
7273 assert!(generic.connected.contains(&covering_id));
7274 assert!(generic.connected.contains(&prefix_id));
7275 assert!(generic.connected.contains(&full_id));
7276 assert!(generic.connected.contains(&no_summary_id));
7277 assert!(!generic.connected.contains(¬_block_sync_id));
7278
7279 let candidates = handle
7280 .block_sync_candidates(
7281 &ZakuraBlockSyncCandidateState {
7282 missing_block_bodies: vec![block::Height(10), block::Height(20)],
7283 ..ZakuraBlockSyncCandidateState::default()
7284 },
7285 false,
7286 &[],
7287 )
7288 .await;
7289
7290 assert_eq!(candidates.connected[0], covering_id);
7291 let prefix_position = candidates
7292 .connected
7293 .iter()
7294 .position(|node_id| node_id == &prefix_id)
7295 .expect("prefix-covering peer remains a candidate");
7296 let no_summary_position = candidates
7297 .connected
7298 .iter()
7299 .position(|node_id| node_id == &no_summary_id)
7300 .expect("no-summary peer remains a candidate");
7301 assert!(prefix_position < no_summary_position);
7302 assert!(candidates.connected.contains(&no_summary_id));
7303 assert!(!candidates.connected.contains(&full_id));
7304 assert!(!candidates.connected.contains(¬_block_sync_id));
7305
7306 let candidates = handle
7307 .block_sync_candidates(
7308 &ZakuraBlockSyncCandidateState {
7309 missing_block_bodies: vec![block::Height(10), block::Height(20)],
7310 admitted_node_ids: vec![full_id],
7311 },
7312 false,
7313 &[],
7314 )
7315 .await;
7316
7317 assert!(candidates.connected.contains(&full_id));
7318 }
7319
7320 #[tokio::test]
7321 async fn first_party_block_sync_summary_only_imports_from_authenticated_peer() {
7322 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7323 let handle = discovery_handle_with_connected(connected_rx);
7324 let peer_node_id = secret_key().public();
7325 let claimed_node_id = secret_key().public();
7326 let now = current_unix_secs();
7327 connected_tx.send_replace(vec![peer_id_for(peer_node_id)]);
7328
7329 let error = handle
7330 .import_connected_peer_services_at(
7331 first_party_services(
7332 claimed_node_id,
7333 now + 30,
7334 vec![ServiceSummaryEnvelope::block_sync(&block_sync_summary())
7335 .expect("test block summary encodes")],
7336 ),
7337 peer_node_id,
7338 now,
7339 )
7340 .await
7341 .expect_err("mismatched first-party block summary is rejected");
7342
7343 assert!(matches!(
7344 error,
7345 DiscoveryWireError::MismatchedNodeId {
7346 field: "services node id"
7347 }
7348 ));
7349 assert_eq!(
7350 handle.live_service_summaries_at(peer_node_id, now).await,
7351 None
7352 );
7353 assert_eq!(
7354 handle.live_service_summaries_at(claimed_node_id, now).await,
7355 None
7356 );
7357
7358 let record = runtime_record_with(10, ZakuraServiceId::block_sync(), test_addr(10));
7359 let node_id = record.body.node_id;
7360 let message = DiscoveryMessage::Peers {
7361 records: vec![record],
7362 };
7363 let encoded = message.encode().expect("test peers response encodes");
7364 let DiscoveryMessage::Peers { records } =
7365 DiscoveryMessage::decode(&encoded).expect("test peers response decodes")
7366 else {
7367 panic!("PEERS response decoded as a different message");
7368 };
7369
7370 handle.import_peer_records(records, None).await;
7371 connected_tx.send_replace(vec![peer_id_for(peer_node_id), peer_id_for(node_id)]);
7372
7373 assert_eq!(handle.live_service_summaries_at(node_id, now).await, None);
7374 assert_eq!(handle.active_services(node_id).await, None);
7375 }
7376
7377 #[tokio::test]
7378 async fn block_sync_summary_mismatch_changes_selection_without_punitive_state() {
7379 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7380 let handle = discovery_handle_with_connected(connected_rx);
7381 let now = current_unix_secs();
7382 let peer = runtime_record_with(1, ZakuraServiceId::block_sync(), test_addr(1));
7383 let covering_peer = runtime_record_with(2, ZakuraServiceId::block_sync(), test_addr(2));
7384 let peer_id = peer.body.node_id;
7385 let covering_peer_id = covering_peer.body.node_id;
7386 connected_tx.send_replace(vec![peer_id_for(peer_id), peer_id_for(covering_peer_id)]);
7387 for record in [peer, covering_peer] {
7388 let node_id = record.body.node_id;
7389 handle
7390 .import_connected_peer_record(record, node_id)
7391 .await
7392 .expect("connected record imports");
7393 }
7394
7395 let mut summary = block_sync_summary();
7396 summary.servable_low = block::Height(10);
7397 summary.servable_high = block::Height(20);
7398 summary.free_slots = 3;
7399 let mut covering_summary = summary;
7400 covering_summary.free_slots = 1;
7401 for (node_id, summary) in [(peer_id, summary), (covering_peer_id, covering_summary)] {
7402 handle
7403 .import_connected_peer_services_at(
7404 first_party_services(
7405 node_id,
7406 now + 30,
7407 vec![ServiceSummaryEnvelope::block_sync(&summary)
7408 .expect("test block summary encodes")],
7409 ),
7410 node_id,
7411 now,
7412 )
7413 .await
7414 .expect("first-party block summary imports");
7415 }
7416
7417 let state = ZakuraBlockSyncCandidateState {
7418 missing_block_bodies: vec![block::Height(10), block::Height(20)],
7419 ..ZakuraBlockSyncCandidateState::default()
7420 };
7421 assert_eq!(
7422 handle
7423 .block_sync_candidates(&state, false, &[])
7424 .await
7425 .connected
7426 .first(),
7427 Some(&peer_id)
7428 );
7429
7430 summary.servable_high = block::Height(11);
7431 handle
7432 .import_connected_peer_services_at(
7433 first_party_services(
7434 peer_id,
7435 now + 31,
7436 vec![ServiceSummaryEnvelope::block_sync(&summary)
7437 .expect("test block summary encodes")],
7438 ),
7439 peer_id,
7440 now + 1,
7441 )
7442 .await
7443 .expect("corrected first-party block summary imports");
7444
7445 let candidates = handle.block_sync_candidates(&state, false, &[]).await;
7446 assert_eq!(candidates.connected.first(), Some(&covering_peer_id));
7447 assert!(candidates.connected.contains(&peer_id));
7448 let cached = handle
7449 .live_service_summaries_at(peer_id, now + 1)
7450 .await
7451 .expect("live summary remains cached");
7452 assert_eq!(
7453 cached[0].summary,
7454 ZakuraLiveServiceSummary::BlockSync(summary)
7455 );
7456 }
7457
7458 #[tokio::test]
7459 async fn live_summaries_expire_independently_of_signed_records() {
7460 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7461 let handle = discovery_handle_with_connected(connected_rx);
7462 let record = runtime_record_with(1, ZakuraServiceId::discovery(), test_addr(1));
7463 let node_id = record.body.node_id;
7464 connected_tx.send_replace(vec![peer_id_for(node_id)]);
7465
7466 handle
7467 .import_connected_peer_record(record.clone(), node_id)
7468 .await
7469 .expect("connected self-record imports");
7470 handle
7471 .import_connected_peer_services_at(
7472 first_party_services(
7473 node_id,
7474 NOW + 5,
7475 vec![ServiceSummaryEnvelope::discovery(&discovery_summary())
7476 .expect("test discovery summary encodes")],
7477 ),
7478 node_id,
7479 NOW,
7480 )
7481 .await
7482 .expect("first-party discovery summary imports");
7483
7484 assert_eq!(
7485 handle
7486 .live_service_summaries_at(node_id, NOW)
7487 .await
7488 .expect("fresh summary is cached")
7489 .len(),
7490 1
7491 );
7492 assert_eq!(
7493 handle.live_service_summaries_at(node_id, NOW + 6).await,
7494 Some(Vec::new())
7495 );
7496 assert_eq!(handle.record_for(node_id).await, Some(record));
7497 assert_eq!(
7498 handle.active_services(node_id).await,
7499 Some(vec![ZakuraServiceId::discovery()])
7500 );
7501 }
7502
7503 #[tokio::test]
7504 async fn expired_live_only_service_membership_is_pruned() {
7505 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7506 let handle = discovery_handle_with_connected(connected_rx);
7507 let service = service(42);
7508 let live_only_node_id = secret_key().public();
7509 let record_backed = runtime_record_with(1, service.clone(), test_addr(42));
7510 let record_backed_node_id = record_backed.body.node_id;
7511 let now = current_unix_secs();
7512 connected_tx.send_replace(vec![
7513 peer_id_for(live_only_node_id),
7514 peer_id_for(record_backed_node_id),
7515 ]);
7516
7517 handle
7518 .import_connected_peer_record(record_backed, record_backed_node_id)
7519 .await
7520 .expect("connected record imports");
7521 for node_id in [live_only_node_id, record_backed_node_id] {
7522 handle
7523 .import_connected_peer_services_at(
7524 first_party_services(
7525 node_id,
7526 now + 30,
7527 vec![ServiceSummaryEnvelope {
7528 service_id: service.clone(),
7529 summary_tag: 999,
7530 summary_bytes: vec![1, 2, 3],
7531 }],
7532 ),
7533 node_id,
7534 now,
7535 )
7536 .await
7537 .expect("first-party live summary imports");
7538 }
7539
7540 assert_eq!(
7541 handle.active_services(live_only_node_id).await,
7542 Some(vec![service.clone()])
7543 );
7544 assert_eq!(
7545 handle.active_services(record_backed_node_id).await,
7546 Some(vec![service.clone()])
7547 );
7548 let fresh_candidates = handle
7549 .service_candidates(&service, false, &[])
7550 .await
7551 .connected;
7552 assert!(fresh_candidates.contains(&live_only_node_id));
7553 assert!(fresh_candidates.contains(&record_backed_node_id));
7554
7555 for node_id in [live_only_node_id, record_backed_node_id] {
7556 handle
7557 .import_connected_peer_services_at(
7558 first_party_services(
7559 node_id,
7560 now,
7561 vec![ServiceSummaryEnvelope {
7562 service_id: service.clone(),
7563 summary_tag: 999,
7564 summary_bytes: vec![1, 2, 3],
7565 }],
7566 ),
7567 node_id,
7568 now,
7569 )
7570 .await
7571 .expect("expired first-party live summary is dropped");
7572 }
7573
7574 assert_eq!(handle.active_services(live_only_node_id).await, None);
7575 assert_eq!(
7576 handle.active_services(record_backed_node_id).await,
7577 Some(vec![service.clone()])
7578 );
7579 let stale_candidates = handle
7580 .service_candidates(&service, false, &[])
7581 .await
7582 .connected;
7583 assert!(!stale_candidates.contains(&live_only_node_id));
7584 assert!(stale_candidates.contains(&record_backed_node_id));
7585 }
7586
7587 #[tokio::test]
7588 async fn handle_imports_records_through_storage_validation() {
7589 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
7590 let handle = discovery_handle_with_connected(connected_rx);
7591 let valid = runtime_record_with(1, service(1), test_addr(1));
7592 let signing_secret = secret_key();
7593 let mut no_addr = body(&signing_secret);
7594 no_addr.direct_addrs = Vec::new();
7595 no_addr.expires_at_unix_secs = current_unix_secs() + DEFAULT_DISCOVERY_RECORD_TTL.as_secs();
7596 let invalid = ZakuraNodeRecord::sign(no_addr, &signing_secret).expect("record signs");
7597
7598 let outcome = handle.import_peer_records([valid, invalid], None).await;
7599
7600 assert_eq!(outcome.attempted, 2);
7601 assert_eq!(outcome.added, 1);
7602 assert_eq!(outcome.rejected, 1);
7603 }
7604
7605 #[tokio::test]
7615 async fn import_peer_records_verifies_signatures_without_holding_global_mutex() {
7616 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
7617 let handle = discovery_handle_with_connected(connected_rx);
7618
7619 let mut batch = Vec::with_capacity(MAX_DISCOVERY_RECORDS_PER_RESPONSE);
7623 for index in 0..MAX_DISCOVERY_RECORDS_PER_RESPONSE {
7624 let octet = u8::try_from(index + 1).expect("test batch index fits in u8");
7625 let sequence = u64::try_from(index + 1).expect("test batch index fits in u64");
7626 let mut record = runtime_record_with(sequence, service(1), test_addr(octet));
7627 record.body.sequence += 1;
7628 batch.push(record);
7629 }
7630
7631 let guard = handle.inner.lock().await;
7633
7634 let outcome = tokio::time::timeout(
7637 Duration::from_secs(5),
7638 handle.import_peer_records(batch, None),
7639 )
7640 .await
7641 .expect("import_peer_records must verify signatures without holding the global mutex");
7642
7643 drop(guard);
7644
7645 assert_eq!(outcome.attempted, MAX_DISCOVERY_RECORDS_PER_RESPONSE);
7646 assert_eq!(outcome.rejected, MAX_DISCOVERY_RECORDS_PER_RESPONSE);
7647 assert_eq!(outcome.added, 0);
7648 }
7649
7650 #[test]
7664 fn sample_peers_clone_cost_is_bounded_by_returned_sample() {
7665 const BOOK: usize = 1024;
7666 const ITERS: usize = 400;
7667
7668 let wanted = service(1);
7669 let addrs: Vec<SocketAddr> = (1u8..=MAX_DIRECT_ADDRS_PER_RECORD as u8)
7674 .map(test_addr)
7675 .collect();
7676 let mut services = vec![wanted.clone()];
7677 services.extend((100..100 + (MAX_SERVICES_PER_RECORD - 1)).map(service));
7678
7679 let mut book = ZakuraDiscoveryBook::new(ZakuraDiscoveryBookLimits {
7680 max_records: BOOK,
7681 ..ZakuraDiscoveryBookLimits::default()
7682 });
7683 for seq in 0..BOOK {
7684 let secret = secret_key();
7685 let mut record_body = body(&secret);
7686 record_body.sequence = seq as u64 + 1;
7687 record_body.direct_addrs = addrs.clone();
7688 record_body.services = services.clone();
7689 let record = ZakuraNodeRecord::sign(record_body, &secret).expect("test record signs");
7690 book.import_record(record, None, NOW, &context())
7691 .expect("test record imports");
7692 }
7693
7694 let cap = book.limits.max_imported_records_per_response;
7695
7696 let naive_sample =
7699 |book: &ZakuraDiscoveryBook, rng: &mut StdRng| -> Vec<ZakuraNodeRecord> {
7700 book.entries
7701 .iter()
7702 .filter(|(node_id, entry)| {
7703 book.local_node_id != Some(**node_id)
7704 && !entry_is_expired(entry, NOW)
7705 && has_wanted_services(&entry.record, std::slice::from_ref(&wanted))
7706 && has_discovery_dialable_direct_addrs(&entry.record)
7707 })
7708 .map(|(_, entry)| entry.record.clone())
7709 .choose_multiple(rng, cap)
7710 };
7711
7712 let mut rng = StdRng::seed_from_u64(5);
7713
7714 let _ = naive_sample(&book, &mut rng);
7716 let _ = book.sample_peers(cap, std::slice::from_ref(&wanted), &[], NOW, &mut rng);
7717
7718 let naive_start = std::time::Instant::now();
7719 for _ in 0..ITERS {
7720 assert_eq!(naive_sample(&book, &mut rng).len(), cap);
7721 }
7722 let naive_time = naive_start.elapsed();
7723
7724 let production_start = std::time::Instant::now();
7725 for _ in 0..ITERS {
7726 assert_eq!(
7727 book.sample_peers(cap, std::slice::from_ref(&wanted), &[], NOW, &mut rng)
7728 .len(),
7729 cap
7730 );
7731 }
7732 let production_time = production_start.elapsed();
7733
7734 assert!(
7741 production_time.as_nanos() * 5 < naive_time.as_nanos() * 4,
7742 "sample_peers did not bound its clone work: production={production_time:?} \
7743 clone-everything reference={naive_time:?}; production should be clearly cheaper than \
7744 cloning every filtered record"
7745 );
7746 }
7747
7748 #[test]
7757 fn dial_candidates_selects_top_priority_subset_under_limit() {
7758 let mut book = ZakuraDiscoveryBook::default();
7759 let records = (1u8..=10)
7760 .map(|index| signed_record_with(index.into(), service(1), test_addr(index)))
7761 .collect::<Vec<_>>();
7762 for record in &records {
7763 import_confirmed_record(&mut book, record.clone()).expect("test record imports");
7764 }
7765
7766 book.mark_dial_success(&records[5].body.node_id, NOW + 1);
7769 book.mark_dial_success(&records[3].body.node_id, NOW + 2);
7770 book.mark_dial_success(&records[7].body.node_id, NOW + 3);
7771 let expected_top = vec![
7772 records[7].body.node_id,
7773 records[3].body.node_id,
7774 records[5].body.node_id,
7775 ];
7776
7777 let mut rng = StdRng::seed_from_u64(99);
7778 let selected = book.dial_candidates(
7779 3,
7780 &[service(1)],
7781 DialCandidateExclusions {
7782 connected_node_ids: &[],
7783 in_flight_node_ids: &[],
7784 },
7785 NOW,
7786 (
7787 DEFAULT_DISCOVERY_DIAL_BACKOFF_BASE,
7788 DEFAULT_DISCOVERY_DIAL_BACKOFF_MAX,
7789 ),
7790 &mut rng,
7791 );
7792
7793 assert_eq!(
7794 selected
7795 .iter()
7796 .map(|candidate| candidate.node_id)
7797 .collect::<Vec<_>>(),
7798 expected_top,
7799 );
7800 }
7801
7802 #[tokio::test]
7803 async fn handle_samples_records_through_storage_path() {
7804 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
7805 let handle = discovery_handle_with_connected(connected_rx);
7806 let wanted = service(1);
7807 let matching = runtime_record_with(1, wanted.clone(), test_addr(1));
7808 let excluded = runtime_record_with(2, wanted.clone(), test_addr(2));
7809 let excluded_id = excluded.body.node_id;
7810 let other = runtime_record_with(3, service(2), test_addr(3));
7811 handle
7812 .import_peer_records([matching.clone(), excluded, other], None)
7813 .await;
7814
7815 let sample = handle
7816 .sample_peers(10, std::slice::from_ref(&wanted), &[excluded_id])
7817 .await;
7818
7819 assert_eq!(sample, vec![matching]);
7820 }
7821
7822 #[tokio::test]
7823 async fn stale_discovery_admit_cannot_overwrite_newer_session() {
7824 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
7825 let handle = discovery_handle_with_connected(connected_rx);
7826 let peer = peer_id_for(secret_key().public());
7827
7828 assert_eq!(
7829 handle
7830 .admit_peer(2, peer.clone(), ServicePeerDirection::Inbound)
7831 .await,
7832 ServiceAdmissionDecision::Admit
7833 );
7834 assert_eq!(handle.peer_snapshot().inbound_peers, 1);
7835
7836 assert_eq!(
7837 handle
7838 .admit_peer(1, peer.clone(), ServicePeerDirection::Inbound)
7839 .await,
7840 ServiceAdmissionDecision::RejectNotUseful
7841 );
7842 assert_eq!(handle.peer_snapshot().inbound_peers, 1);
7843
7844 handle.remove_peer(&peer, 1).await;
7845 assert_eq!(
7846 handle.peer_snapshot().inbound_peers,
7847 1,
7848 "stale discovery cleanup must not remove the newer admission",
7849 );
7850
7851 handle.remove_peer(&peer, 2).await;
7852 assert_eq!(handle.peer_snapshot().inbound_peers, 0);
7853 }
7854
7855 #[tokio::test]
7856 async fn discovery_state_construction_works_with_default_config() {
7857 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
7858 let handle = ZakuraDiscoveryHandle::new(
7859 local_config_with(secret_key(), vec![test_addr(46)], vec![service(1)]),
7860 ZakuraDiscoveryConfig::default(),
7861 connected_rx,
7862 )
7863 .expect("default discovery state constructs");
7864
7865 assert_eq!(
7866 handle.current_self_record().body.direct_addrs,
7867 vec![test_addr(46)]
7868 );
7869 }
7870
7871 #[tokio::test]
7872 async fn dial_candidates_exclude_connected_peers_and_respect_soft_cap() {
7873 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7874 let handle = discovery_handle_at(
7875 local_config_with(secret_key(), vec![test_addr(47)], vec![service(1)]),
7876 ZakuraDiscoveryConfig {
7877 max_zakura_connections: 2,
7878 discovery_connection_headroom: 1,
7879 ..ZakuraDiscoveryConfig::default()
7880 },
7881 connected_rx,
7882 NOW,
7883 );
7884 let candidate = runtime_record_with(1, service(1), test_addr(1));
7885 let connected = runtime_record_with(2, service(1), test_addr(2));
7886 let connected_id = connected.body.node_id;
7887 let candidate_id = candidate.body.node_id;
7888 handle
7889 .import_peer_record(candidate.clone(), Some(candidate_id))
7890 .await
7891 .expect("candidate first-party record imports");
7892 handle
7893 .import_connected_peer_record(connected, connected_id)
7894 .await
7895 .expect("connected self-record imports");
7896
7897 connected_tx.send_replace(vec![peer_id_for(connected_id)]);
7898 assert!(handle.dial_candidates(&[service(1)], &[]).await.is_empty());
7899
7900 let (connected_tx, connected_rx) = watch::channel(Vec::new());
7901 let handle = discovery_handle_at(
7902 local_config_with(secret_key(), vec![test_addr(48)], vec![service(1)]),
7903 ZakuraDiscoveryConfig {
7904 max_zakura_connections: 4,
7905 discovery_connection_headroom: 1,
7906 ..ZakuraDiscoveryConfig::default()
7907 },
7908 connected_rx,
7909 NOW,
7910 );
7911 let connected = runtime_record_with(2, service(1), test_addr(2));
7912 let connected_id = connected.body.node_id;
7913 let candidate_id = candidate.body.node_id;
7914 handle
7915 .import_peer_record(candidate.clone(), Some(candidate_id))
7916 .await
7917 .expect("candidate first-party record imports");
7918 handle
7919 .import_connected_peer_record(connected, connected_id)
7920 .await
7921 .expect("connected self-record imports");
7922
7923 connected_tx.send_replace(vec![peer_id_for(connected_id)]);
7924 assert_eq!(
7925 handle.dial_candidates(&[service(1)], &[]).await,
7926 vec![candidate_for(&candidate, false)]
7927 );
7928 }
7929
7930 #[tokio::test]
7931 async fn dial_candidates_exclude_in_flight_peers() {
7932 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
7933 let handle = discovery_handle_with_connected(connected_rx);
7934 let candidate = runtime_record_with(1, service(1), test_addr(1));
7935 let candidate_id = candidate.body.node_id;
7936 handle
7937 .import_peer_record(candidate, Some(secret_key().public()))
7938 .await
7939 .expect("candidate imports");
7940
7941 assert!(handle
7942 .dial_candidates(&[service(1)], &[candidate_id])
7943 .await
7944 .is_empty());
7945 }
7946
7947 #[test]
7948 fn discovery_dial_slot_limit_reserves_headroom_and_concurrent_cap() {
7949 assert_eq!(discovery_dial_slot_limit(0, 0, 8, 2, 4), 4);
7950 assert_eq!(discovery_dial_slot_limit(5, 0, 8, 2, 4), 1);
7951 assert_eq!(discovery_dial_slot_limit(5, 1, 8, 2, 4), 1);
7952 assert_eq!(discovery_dial_slot_limit(2, 4, 8, 2, 4), 0);
7953 assert_eq!(discovery_dial_slot_limit(2, 0, 2, 4, 4), 0);
7954 }
7955
7956 #[tokio::test]
7957 async fn handle_records_are_owned_after_storage_changes() {
7958 let (_connected_tx, connected_rx) = watch::channel(Vec::new());
7959 let handle = discovery_handle_with_connected(connected_rx);
7960 let secret = secret_key();
7961 let old = {
7962 let mut record_body = body(&secret);
7963 record_body.sequence = 1;
7964 record_body.direct_addrs = vec![test_addr(10)];
7965 record_body.services = vec![service(1)];
7966 record_body.expires_at_unix_secs =
7967 current_unix_secs() + DEFAULT_DISCOVERY_RECORD_TTL.as_secs();
7968 ZakuraNodeRecord::sign(record_body, &secret).expect("old record signs")
7969 };
7970 let new = {
7971 let mut record_body = body(&secret);
7972 record_body.sequence = 2;
7973 record_body.direct_addrs = vec![test_addr(11)];
7974 record_body.services = vec![service(1)];
7975 record_body.expires_at_unix_secs =
7976 current_unix_secs() + DEFAULT_DISCOVERY_RECORD_TTL.as_secs();
7977 ZakuraNodeRecord::sign(record_body, &secret).expect("new record signs")
7978 };
7979
7980 handle.import_peer_records([old.clone()], None).await;
7981 let sample = handle.sample_peers(1, &[service(1)], &[]).await;
7982 handle.import_peer_records([new], None).await;
7983
7984 assert_eq!(sample, vec![old]);
7985 }
7986}