1use std::{
2 fmt::{Display, Formatter},
3 net::ToSocketAddrs,
4 num::ParseIntError,
5 str::FromStr,
6 time::Duration,
7};
8
9use hopr_api::Multiaddr;
10pub use hopr_protocol_hopr::{HoprCodecConfig, HoprUnacknowledgedTicketProcessorConfig, SurbPopOrder, SurbStoreConfig};
11use hopr_protocol_pix::SsaReconstructorConfig;
12pub use hopr_transport_mixer::config::MixerConfig;
13pub use hopr_transport_probe::config::ProbeConfig;
14use hopr_transport_session::{
15 DEFAULT_MAX_SSAS_PER_SSA_REQUEST, DEFAULT_PIX_POLYS_PER_SSA, DEFAULT_PIX_SHARES_PER_POLY, IncomingSessionPixConfig,
16 MAX_SSA_BATCH_SIZE, MIN_BALANCER_SAMPLING_INTERVAL, MIN_SURB_BUFFER_DURATION,
17};
18use proc_macro_regex::regex;
19use validator::{Validate, ValidationError, ValidationErrors};
20
21use crate::{errors::HoprTransportError, protocol::PacketPipelineConfig};
22
23const DEFAULT_COUNTER_FLUSH_INTERVAL: Duration = Duration::from_secs(15);
24
25const DEFAULT_PER_PEER_CHANNEL_CAPACITY: usize = 5_000;
26const DEFAULT_STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(2);
27const DEFAULT_FRAME_WRITER_BACKPRESSURE_BYTES: usize = 131_072;
28const DEFAULT_EGRESS_BACKPRESSURE_TIMEOUT: Duration = Duration::from_secs(2);
29
30pub const MIN_STREAM_OPEN_TIMEOUT: Duration = Duration::from_millis(1);
32
33pub const MIN_EGRESS_BACKPRESSURE_TIMEOUT: Duration = Duration::from_millis(1);
35
36fn default_per_peer_channel_capacity() -> usize {
37 DEFAULT_PER_PEER_CHANNEL_CAPACITY
38}
39
40fn default_stream_open_timeout() -> Duration {
41 DEFAULT_STREAM_OPEN_TIMEOUT
42}
43
44fn default_frame_writer_backpressure_bytes() -> usize {
45 DEFAULT_FRAME_WRITER_BACKPRESSURE_BYTES
46}
47
48#[inline]
49fn default_egress_backpressure_timeout() -> Duration {
50 DEFAULT_EGRESS_BACKPRESSURE_TIMEOUT
51}
52
53fn validate_stream_open_timeout(value: &Duration) -> Result<(), ValidationError> {
54 if MIN_STREAM_OPEN_TIMEOUT <= *value {
55 Ok(())
56 } else {
57 Err(ValidationError::new("stream open timeout must be at least 1 ms"))
58 }
59}
60
61fn validate_egress_backpressure_timeout(value: &Duration) -> Result<(), ValidationError> {
62 if MIN_EGRESS_BACKPRESSURE_TIMEOUT <= *value {
63 Ok(())
64 } else {
65 Err(ValidationError::new(
68 "egress backpressure timeout must be at least 1 ms",
69 ))
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Validate, smart_default::SmartDefault)]
75#[cfg_attr(
76 feature = "serde",
77 derive(serde::Serialize, serde::Deserialize),
78 serde(deny_unknown_fields)
79)]
80pub struct StreamProtocolConfig {
81 #[validate(range(min = 1))]
98 #[default(default_per_peer_channel_capacity())]
99 #[cfg_attr(feature = "serde", serde(default = "default_per_peer_channel_capacity"))]
100 pub per_peer_channel_capacity: usize,
101
102 #[validate(custom(function = "validate_stream_open_timeout"))]
113 #[default(default_stream_open_timeout())]
114 #[cfg_attr(
115 feature = "serde",
116 serde(default = "default_stream_open_timeout", with = "humantime_serde")
117 )]
118 pub stream_open_timeout: Duration,
119
120 #[validate(range(min = 1))]
130 #[default(default_frame_writer_backpressure_bytes())]
131 #[cfg_attr(feature = "serde", serde(default = "default_frame_writer_backpressure_bytes"))]
132 pub frame_writer_backpressure_bytes: usize,
133
134 #[validate(custom(function = "validate_egress_backpressure_timeout"))]
146 #[default(default_egress_backpressure_timeout())]
147 #[cfg_attr(feature = "serde", serde(default = "default_egress_backpressure_timeout"))]
148 pub egress_backpressure_timeout: Duration,
149}
150
151fn default_counter_flush_interval() -> Duration {
152 DEFAULT_COUNTER_FLUSH_INTERVAL
153}
154
155const DEFAULT_SURB_FLUSH_INTERVAL: Duration = Duration::from_secs(1);
162
163fn default_surb_flush_interval() -> Duration {
164 DEFAULT_SURB_FLUSH_INTERVAL
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, smart_default::SmartDefault)]
178#[cfg_attr(
179 feature = "serde",
180 derive(serde::Serialize, serde::Deserialize),
181 serde(deny_unknown_fields)
182)]
183pub struct TransitLatencyConfig {
184 #[default(Duration::from_millis(50))]
186 #[cfg_attr(feature = "serde", serde(with = "humantime_serde"))]
187 pub mean: Duration,
188 #[default(Duration::from_millis(5))]
192 #[cfg_attr(feature = "serde", serde(with = "humantime_serde"))]
193 pub std_dev: Duration,
194}
195
196#[derive(Debug, smart_default::SmartDefault, Validate, Clone, PartialEq)]
198#[cfg_attr(
199 feature = "serde",
200 derive(serde::Serialize, serde::Deserialize),
201 serde(deny_unknown_fields)
202)]
203pub struct HoprProtocolConfig {
204 #[validate(nested)]
206 #[cfg_attr(feature = "serde", serde(default))]
207 pub transport: TransportConfig,
208 #[validate(nested)]
210 #[cfg_attr(feature = "serde", serde(default))]
211 pub packet: HoprPacketPipelineConfig,
212 #[validate(nested)]
214 #[cfg_attr(feature = "serde", serde(default))]
215 pub probe: ProbeConfig,
216 #[validate(nested)]
218 #[cfg_attr(feature = "serde", serde(default))]
219 pub session: SessionGlobalConfig,
220 #[validate(nested)]
222 #[cfg_attr(feature = "serde", serde(default))]
223 pub pix: PixGlobalConfig,
224 #[validate(custom(function = "validate_incoming_session_pix_config"))]
226 #[cfg_attr(feature = "serde", serde(default))]
227 pub incoming_session_pix_config: IncomingSessionPixConfig,
228 #[cfg_attr(feature = "serde", serde(default))]
230 pub mixer: MixerConfig,
231 #[cfg_attr(feature = "serde", serde(default))]
237 pub transit_latency: Option<TransitLatencyConfig>,
238 #[validate(nested)]
240 #[cfg_attr(feature = "serde", serde(default))]
241 pub stream: StreamProtocolConfig,
242 #[validate(nested)]
244 #[cfg_attr(feature = "serde", serde(skip))]
245 pub path_planner: crate::path::PathPlannerConfig,
246 #[default(default_counter_flush_interval())]
251 #[cfg_attr(
252 feature = "serde",
253 serde(default = "default_counter_flush_interval", with = "humantime_serde")
254 )]
255 pub counter_flush_interval: Duration,
256 #[default(default_surb_flush_interval())]
260 #[cfg_attr(
261 feature = "serde",
262 serde(default = "default_surb_flush_interval", with = "humantime_serde")
263 )]
264 pub surb_flush_interval: Duration,
265}
266
267fn validate_incoming_session_pix_config(cfg: &IncomingSessionPixConfig) -> Result<(), ValidationError> {
281 if cfg.quota_range.is_empty() {
282 return Err(ValidationError::new(
283 "pix quota_range must be non-empty (start must not exceed end)",
284 ));
285 }
286 if !(1..=MAX_SSA_BATCH_SIZE).contains(&cfg.ssas_per_request) {
287 return Err(ValidationError::new(
288 "pix ssas_per_request must be between 1 and MAX_SSA_BATCH_SIZE",
289 ));
290 }
291 Ok(())
292}
293
294const MAX_PIX_DIMENSION_PRODUCT_FACTOR: usize = 4;
299
300fn validate_pix_dimension_product(cfg: &PixGlobalConfig) -> Result<(), ValidationError> {
320 const PROFILED: usize = DEFAULT_PIX_POLYS_PER_SSA as usize * DEFAULT_PIX_SHARES_PER_POLY as usize;
321
322 if cfg.num_ssa_parts.saturating_mul(cfg.ssa_part_size) > MAX_PIX_DIMENSION_PRODUCT_FACTOR * PROFILED {
323 return Err(ValidationError::new(
324 "num_ssa_parts * ssa_part_size exceeds the supported per-cycle dimension product",
325 ));
326 }
327
328 if cfg.surplus_shares() > cfg.ssa_part_size {
334 return Err(ValidationError::new(
335 "additional_shares must not exceed ssa_part_size — the surplus is billed, so this pays for more \
336 redundancy than payload",
337 ));
338 }
339 Ok(())
340}
341
342#[derive(Clone, Copy, Debug, PartialEq, Validate, smart_default::SmartDefault)]
344#[cfg_attr(
345 feature = "serde",
346 derive(serde::Serialize, serde::Deserialize),
347 serde(default, deny_unknown_fields)
348)]
349#[validate(schema(function = "validate_pix_dimension_product", skip_on_field_errors = false))]
350pub struct PixGlobalConfig {
351 #[validate(range(min = 8, max = 16192))]
363 #[default(DEFAULT_PIX_POLYS_PER_SSA as usize)]
364 pub num_ssa_parts: usize,
365
366 #[validate(range(min = 2, max = 255))]
377 #[default(DEFAULT_PIX_SHARES_PER_POLY as usize)]
378 pub ssa_part_size: usize,
379
380 #[validate(range(min = 0, max = 255))]
412 pub additional_shares: Option<usize>,
413
414 #[validate(range(min = 1, max = 20))]
433 #[default(DEFAULT_MAX_SSAS_PER_SSA_REQUEST)]
434 pub max_ssas_per_request: usize,
435
436 #[validate(nested)]
441 #[cfg_attr(feature = "serde", serde(default))]
442 pub reconstructor: PixReconstructorConfig,
443}
444
445impl PixGlobalConfig {
446 pub fn surplus_shares(&self) -> usize {
454 self.additional_shares.unwrap_or_else(|| {
455 hopr_protocol_pix::default_surplus_for(self.ssa_part_size.min(u8::MAX as usize) as u8) as usize
456 })
457 }
458}
459
460fn validate_pix_reconstructor_config(cfg: &PixReconstructorConfig) -> Result<(), ValidationError> {
471 SsaReconstructorConfig::from(*cfg).validate().map_err(|errors| {
472 let mut error = ValidationError::new("pix reconstructor configuration is out of range");
473 error.message = Some(errors.to_string().into());
474 error
475 })
476}
477
478#[derive(Clone, Copy, Debug, PartialEq, Validate, smart_default::SmartDefault)]
499#[cfg_attr(
500 feature = "serde",
501 derive(serde::Serialize, serde::Deserialize),
502 serde(default, deny_unknown_fields)
503)]
504#[validate(schema(function = "validate_pix_reconstructor_config", skip_on_field_errors = false))]
505pub struct PixReconstructorConfig {
506 #[default(SsaReconstructorConfig::DEFAULT_INCOMPLETE_COMMITMENT_LIFETIME)]
511 #[cfg_attr(feature = "serde", serde(with = "humantime_serde"))]
512 pub incomplete_commitment_lifetime: Duration,
513
514 #[default(SsaReconstructorConfig::DEFAULT_UNUSED_VERIFIER_LIFETIME)]
518 #[cfg_attr(feature = "serde", serde(with = "humantime_serde"))]
519 pub unused_verifier_lifetime: Duration,
520
521 #[default(SsaReconstructorConfig::DEFAULT_MAX_TRACKED_PEERS)]
525 pub max_tracked_peers: usize,
526
527 #[default(SsaReconstructorConfig::DEFAULT_MAX_AWAITING_ACKS)]
531 pub max_awaiting_acks: usize,
532
533 #[default(SsaReconstructorConfig::DEFAULT_MAX_ACK_AWAIT_TIME)]
537 #[cfg_attr(feature = "serde", serde(with = "humantime_serde"))]
538 pub max_ack_await_time: Duration,
539
540 #[default(SsaReconstructorConfig::DEFAULT_USE_BATCH_VERIFICATION)]
545 pub use_batch_verification: bool,
546
547 #[default(SsaReconstructorConfig::DEFAULT_EARLY_RECOVERY_THRESHOLD)]
552 pub early_recovery_threshold: f64,
553
554 #[default(SsaReconstructorConfig::DEFAULT_MAX_ACK_BUFFER_BYTES)]
568 pub max_ack_buffer_bytes: usize,
569}
570
571impl From<PixReconstructorConfig> for SsaReconstructorConfig {
572 fn from(cfg: PixReconstructorConfig) -> Self {
579 let PixReconstructorConfig {
580 incomplete_commitment_lifetime,
581 unused_verifier_lifetime,
582 max_tracked_peers,
583 max_awaiting_acks,
584 max_ack_await_time,
585 use_batch_verification,
586 early_recovery_threshold,
587 max_ack_buffer_bytes,
588 } = cfg;
589
590 Self {
591 incomplete_commitment_lifetime,
592 unused_verifier_lifetime,
593 max_tracked_peers,
594 max_awaiting_acks,
595 max_ack_await_time,
596 use_batch_verification,
597 early_recovery_threshold,
598 max_ack_buffer_bytes,
599 }
600 }
601}
602
603#[derive(Clone, Copy, Debug, PartialEq, Validate, smart_default::SmartDefault)]
605#[cfg_attr(
606 feature = "serde",
607 derive(serde::Serialize, serde::Deserialize),
608 serde(deny_unknown_fields)
609)]
610pub struct HoprPacketPipelineConfig {
611 #[validate(nested)]
613 #[cfg_attr(feature = "serde", serde(default))]
614 pub codec: HoprCodecConfig,
615 #[validate(nested)]
617 #[cfg_attr(feature = "serde", serde(default))]
618 pub ack_processor: HoprUnacknowledgedTicketProcessorConfig,
619 #[validate(nested)]
621 #[cfg_attr(feature = "serde", serde(default))]
622 pub surb_store: SurbStoreConfig,
623 #[validate(nested)]
625 #[cfg_attr(feature = "serde", serde(default))]
626 pub pipeline: PacketPipelineConfig,
627}
628
629regex!(is_dns_address_regex "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)*[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$");
630
631#[inline]
633pub fn looks_like_domain(s: &str) -> bool {
634 is_dns_address_regex(s)
635}
636
637pub fn is_reachable_domain(host: &str) -> bool {
639 host.to_socket_addrs().is_ok_and(|i| i.into_iter().next().is_some())
640}
641
642#[derive(Debug, Clone, PartialEq)]
644#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
645pub enum HostType {
646 IPv4(String),
648 Domain(String),
650}
651
652impl validator::Validate for HostType {
653 fn validate(&self) -> Result<(), ValidationErrors> {
654 match &self {
655 HostType::IPv4(ip4) => validate_ipv4_address(ip4).map_err(|e| {
656 let mut errs = ValidationErrors::new();
657 errs.add("ipv4", e);
658 errs
659 }),
660 HostType::Domain(domain) => validate_dns_address(domain).map_err(|e| {
661 let mut errs = ValidationErrors::new();
662 errs.add("domain", e);
663 errs
664 }),
665 }
666 }
667}
668
669impl Default for HostType {
670 fn default() -> Self {
671 HostType::IPv4("127.0.0.1".to_owned())
672 }
673}
674
675#[derive(Debug, Validate, Clone, PartialEq)]
681#[cfg_attr(
682 feature = "serde",
683 derive(serde::Serialize, serde::Deserialize),
684 serde(deny_unknown_fields)
685)]
686pub struct HostConfig {
687 #[cfg_attr(feature = "serde", serde(default))]
689 pub address: HostType,
690 #[validate(range(min = 1u16))]
692 #[cfg_attr(feature = "serde", serde(default))]
693 pub port: u16,
694}
695
696impl FromStr for HostConfig {
697 type Err = String;
698
699 fn from_str(s: &str) -> Result<Self, Self::Err> {
700 let (ip_or_dns, str_port) = match s.split_once(':') {
701 None => return Err("Invalid host, is not in the '<host>:<port>' format".into()),
702 Some(split) => split,
703 };
704
705 let port = str_port.parse().map_err(|e: ParseIntError| e.to_string())?;
706
707 if validator::ValidateIp::validate_ipv4(&ip_or_dns) {
708 Ok(Self {
709 address: HostType::IPv4(ip_or_dns.to_owned()),
710 port,
711 })
712 } else if looks_like_domain(ip_or_dns) {
713 Ok(Self {
714 address: HostType::Domain(ip_or_dns.to_owned()),
715 port,
716 })
717 } else {
718 Err("Not a valid IPv4 or domain host".into())
719 }
720 }
721}
722
723impl Display for HostConfig {
724 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
725 write!(f, "{:?}:{}", self.address, self.port)
726 }
727}
728
729fn default_multiaddr_transport(port: u16) -> String {
730 cfg_if::cfg_if! {
731 if #[cfg(feature = "p2p-announce-quic")] {
732 let on_dappnode = std::env::var("DAPPNODE")
736 .map(|v| v.to_lowercase() == "true")
737 .unwrap_or(false);
738
739 let uses_nat = std::env::var("HOPRD_NAT")
741 .map(|v| v.to_lowercase() == "true")
742 .unwrap_or(on_dappnode);
743
744 if uses_nat {
745 format!("tcp/{port}")
746 } else {
747 format!("udp/{port}/quic-v1")
748 }
749 } else {
750 format!("tcp/{port}")
751 }
752 }
753}
754
755impl TryFrom<&HostConfig> for Multiaddr {
756 type Error = HoprTransportError;
757
758 fn try_from(value: &HostConfig) -> Result<Self, Self::Error> {
759 match &value.address {
760 HostType::IPv4(ip) => Multiaddr::from_str(
761 format!("/ip4/{}/{}", ip.as_str(), default_multiaddr_transport(value.port)).as_str(),
762 )
763 .map_err(|e| HoprTransportError::Api(e.to_string())),
764 HostType::Domain(domain) => Multiaddr::from_str(
765 format!("/dns4/{}/{}", domain.as_str(), default_multiaddr_transport(value.port)).as_str(),
766 )
767 .map_err(|e| HoprTransportError::Api(e.to_string())),
768 }
769 }
770}
771
772fn validate_ipv4_address(s: &str) -> Result<(), ValidationError> {
773 if validator::ValidateIp::validate_ipv4(&s) {
774 let ipv4 = std::net::Ipv4Addr::from_str(s)
775 .map_err(|_| ValidationError::new("Failed to deserialize the string into an ipv4 address"))?;
776
777 if ipv4.is_private() || ipv4.is_multicast() || ipv4.is_unspecified() {
778 return Err(ValidationError::new(
779 "IPv4 cannot be private, multicast or unspecified (0.0.0.0)",
780 ))?;
781 }
782 Ok(())
783 } else {
784 Err(ValidationError::new("Invalid IPv4 address provided"))
785 }
786}
787
788fn validate_dns_address(s: &str) -> Result<(), ValidationError> {
789 if looks_like_domain(s) || is_reachable_domain(s) {
790 Ok(())
791 } else {
792 Err(ValidationError::new("Invalid DNS address provided"))
793 }
794}
795
796#[derive(Debug, Default, Validate, Clone, Copy, PartialEq)]
798#[cfg_attr(
799 feature = "serde",
800 derive(serde::Serialize, serde::Deserialize),
801 serde(deny_unknown_fields)
802)]
803pub struct TransportConfig {
804 #[cfg_attr(feature = "serde", serde(default))]
807 pub announce_local_addresses: bool,
808 #[cfg_attr(feature = "serde", serde(default))]
811 pub prefer_local_addresses: bool,
812}
813
814const DEFAULT_SESSION_IDLE_TIMEOUT: Duration = Duration::from_mins(3);
815
816const SESSION_IDLE_MIN_TIMEOUT: Duration = Duration::from_secs(2);
817
818const DEFAULT_SESSION_ESTABLISH_RETRY_DELAY: Duration = Duration::from_secs(2);
819
820const DEFAULT_SESSION_ESTABLISH_MAX_RETRIES: usize = 3;
821
822const DEFAULT_SESSION_BALANCER_SAMPLING: Duration = Duration::from_millis(100);
823
824const DEFAULT_SESSION_BALANCER_BUFFER_DURATION: Duration = Duration::from_secs(5);
825
826const DEFAULT_MAXIMUM_MANAGED_SESSIONS: usize = 100;
827
828fn default_session_balancer_buffer_duration() -> Duration {
829 DEFAULT_SESSION_BALANCER_BUFFER_DURATION
830}
831
832fn default_session_establish_max_retries() -> usize {
833 DEFAULT_SESSION_ESTABLISH_MAX_RETRIES
834}
835
836fn default_session_idle_timeout() -> Duration {
837 DEFAULT_SESSION_IDLE_TIMEOUT
838}
839
840fn default_session_establish_retry_delay() -> Duration {
841 DEFAULT_SESSION_ESTABLISH_RETRY_DELAY
842}
843
844fn default_session_balancer_sampling() -> Duration {
845 DEFAULT_SESSION_BALANCER_SAMPLING
846}
847
848fn default_max_managed_sessions() -> usize {
849 DEFAULT_MAXIMUM_MANAGED_SESSIONS
850}
851
852fn default_session_surb_balance_notify_period() -> Option<Duration> {
860 Some(Duration::from_secs(15))
861}
862
863fn default_session_max_frames_behind_gap() -> Option<usize> {
864 Some(256)
865}
866
867fn validate_session_idle_timeout(value: &Duration) -> Result<(), ValidationError> {
868 if SESSION_IDLE_MIN_TIMEOUT <= *value {
869 Ok(())
870 } else {
871 Err(ValidationError::new("session idle timeout is too low"))
872 }
873}
874
875fn validate_balancer_sampling(value: &Duration) -> Result<(), ValidationError> {
876 if MIN_BALANCER_SAMPLING_INTERVAL <= *value {
877 Ok(())
878 } else {
879 Err(ValidationError::new("balancer sampling interval is too low"))
880 }
881}
882
883fn validate_balancer_buffer_duration(value: &Duration) -> Result<(), ValidationError> {
884 if MIN_SURB_BUFFER_DURATION <= *value {
885 Ok(())
886 } else {
887 Err(ValidationError::new("minmum SURB buffer duration is too low"))
888 }
889}
890
891fn validate_surb_balance_notify_period(value: &Duration) -> Result<(), ValidationError> {
892 if *value >= Duration::from_secs(1) {
894 Ok(())
895 } else {
896 Err(ValidationError::new(
897 "SURB balance notify period must be at least 1 second",
898 ))
899 }
900}
901
902#[derive(Clone, Copy, Debug, PartialEq, Eq, Validate, smart_default::SmartDefault)]
904#[cfg_attr(
905 feature = "serde",
906 derive(serde::Serialize, serde::Deserialize),
907 serde(deny_unknown_fields)
908)]
909pub struct SessionGlobalConfig {
910 #[validate(custom(function = "validate_session_idle_timeout"))]
914 #[default(default_session_idle_timeout())]
915 #[cfg_attr(
916 feature = "serde",
917 serde(default = "default_session_idle_timeout", with = "humantime_serde")
918 )]
919 pub idle_timeout: Duration,
920
921 #[validate(range(min = 2, max = 100_000))]
925 #[default(default_max_managed_sessions())]
926 #[cfg_attr(feature = "serde", serde(default = "default_max_managed_sessions"))]
927 pub maximum_managed_sessions: usize,
928
929 #[validate(range(min = 0, max = 20))]
934 #[default(default_session_establish_max_retries())]
935 #[cfg_attr(feature = "serde", serde(default = "default_session_establish_max_retries"))]
936 pub establish_max_retries: usize,
937
938 #[default(default_session_establish_retry_delay())]
942 #[cfg_attr(
943 feature = "serde",
944 serde(default = "default_session_establish_retry_delay", with = "humantime_serde")
945 )]
946 pub establish_retry_timeout: Duration,
947
948 #[validate(custom(function = "validate_balancer_sampling"))]
952 #[default(default_session_balancer_sampling())]
953 #[cfg_attr(
954 feature = "serde",
955 serde(default = "default_session_balancer_sampling", with = "humantime_serde")
956 )]
957 pub balancer_sampling_interval: Duration,
958
959 #[validate(custom(function = "validate_balancer_buffer_duration"))]
967 #[default(default_session_balancer_buffer_duration())]
968 #[cfg_attr(
969 feature = "serde",
970 serde(default = "default_session_balancer_buffer_duration", with = "humantime_serde")
971 )]
972 pub balancer_minimum_surb_buffer_duration: Duration,
973
974 #[validate(custom(function = "validate_surb_balance_notify_period"))]
981 #[default(default_session_surb_balance_notify_period())]
982 #[cfg_attr(
983 feature = "serde",
984 serde(
985 default = "default_session_surb_balance_notify_period",
986 with = "humantime_serde::option"
987 )
988 )]
989 pub surb_balance_notify_period: Option<Duration>,
990
991 #[default(default_session_max_frames_behind_gap())]
1002 #[cfg_attr(feature = "serde", serde(default = "default_session_max_frames_behind_gap"))]
1003 pub max_frames_behind_gap: Option<usize>,
1004
1005 #[validate(nested)]
1007 #[cfg_attr(feature = "serde", serde(default))]
1008 pub tag_allocator: hopr_transport_tag_allocator::TagAllocatorConfig,
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014
1015 #[test]
1025 fn default_pix_dimensions_must_be_inside_default_incoming_quota_range() {
1026 let pix = PixGlobalConfig::default();
1027 let incoming = IncomingSessionPixConfig::default();
1028
1029 let quota = pix.num_ssa_parts as u64
1030 * (pix.ssa_part_size + pix.surplus_shares()) as u64
1031 * hopr_crypto_packet::prelude::HoprPacket::PAYLOAD_SIZE as u64;
1032
1033 assert!(
1034 incoming.quota_range.contains("a),
1035 "default PIX quota {quota} is outside the default accepted range {:?} — every PIX session would be \
1036 rejected with UnacceptablePixParams",
1037 incoming.quota_range
1038 );
1039
1040 assert_eq!(
1045 quota,
1046 *incoming.quota_range.end(),
1047 "the accepted range must be anchored at the nominal default quota"
1048 );
1049 }
1050
1051 #[test]
1052 fn default_pix_configs_must_validate() {
1053 PixGlobalConfig::default()
1054 .validate()
1055 .expect("default PixGlobalConfig must be valid");
1056 validate_incoming_session_pix_config(&IncomingSessionPixConfig::default())
1057 .expect("default IncomingSessionPixConfig must be valid");
1058 HoprProtocolConfig::default()
1059 .validate()
1060 .expect("default HoprProtocolConfig must be valid");
1061 }
1062
1063 #[test]
1073 fn pix_reconstructor_mirror_matches_the_protocol_defaults() {
1074 assert_eq!(
1075 SsaReconstructorConfig::default(),
1076 SsaReconstructorConfig::from(PixReconstructorConfig::default()),
1077 "the operator-facing mirror and the reconstructor it configures have drifted apart"
1078 );
1079 }
1080
1081 #[test]
1089 fn the_documented_ack_budget_floor_is_the_enforced_one() {
1090 const DOCUMENTED_FLOOR: usize = 25_600;
1091
1092 PixReconstructorConfig {
1093 max_ack_buffer_bytes: DOCUMENTED_FLOOR,
1094 ..Default::default()
1095 }
1096 .validate()
1097 .expect("the documented floor itself must be accepted");
1098
1099 assert!(
1100 PixReconstructorConfig {
1101 max_ack_buffer_bytes: DOCUMENTED_FLOOR - 1,
1102 ..Default::default()
1103 }
1104 .validate()
1105 .is_err(),
1106 "one byte below the documented floor must be rejected — the prose and the validator have drifted"
1107 );
1108 }
1109
1110 #[test]
1117 fn pix_dimensions_are_bounded_by_their_product_not_only_field_by_field() {
1118 const MAX_NUM_SSA_PARTS: usize = 16192;
1119 const MAX_SSA_PART_SIZE: usize = hopr_protocol_pix::MAX_POLY_THRESHOLD as usize;
1120
1121 let extreme = PixGlobalConfig {
1123 num_ssa_parts: MAX_NUM_SSA_PARTS,
1124 ssa_part_size: MAX_SSA_PART_SIZE,
1125 ..Default::default()
1126 };
1127 assert!(
1128 extreme.validate().is_err(),
1129 "16192 x 255 is ~8x the profiled product and must be rejected"
1130 );
1131
1132 let profiled = DEFAULT_PIX_POLYS_PER_SSA as usize * DEFAULT_PIX_SHARES_PER_POLY as usize;
1137 for (polys, shares) in [(4096usize, 128usize), (8192, 64)] {
1138 assert_eq!(polys * shares, profiled, "test case must hold the product constant");
1139 let cfg = PixGlobalConfig {
1140 num_ssa_parts: polys,
1141 ssa_part_size: shares,
1142 ..Default::default()
1143 };
1144 assert!(
1145 cfg.validate().is_ok(),
1146 "{polys} x {shares} is the profiled product re-split and must stay valid"
1147 );
1148 }
1149
1150 let ceiling = MAX_PIX_DIMENSION_PRODUCT_FACTOR * profiled;
1155 let just_under = PixGlobalConfig {
1156 num_ssa_parts: MAX_NUM_SSA_PARTS,
1157 ssa_part_size: ceiling / MAX_NUM_SSA_PARTS,
1158 ..Default::default()
1159 };
1160 assert!(
1161 just_under.num_ssa_parts * just_under.ssa_part_size <= ceiling,
1162 "test case must sit under the ceiling"
1163 );
1164 assert!(
1165 just_under.validate().is_ok(),
1166 "the largest reachable product under the ceiling must be accepted"
1167 );
1168
1169 let past_ceiling = PixGlobalConfig {
1170 ssa_part_size: just_under.ssa_part_size + 1,
1171 ..just_under
1172 };
1173 assert!(
1174 past_ceiling.num_ssa_parts * past_ceiling.ssa_part_size > ceiling,
1175 "test case must sit over the ceiling"
1176 );
1177 assert!(past_ceiling.validate().is_err(), "past the ceiling must be rejected");
1178 }
1179
1180 #[allow(clippy::reversed_empty_ranges)]
1184 #[test]
1185 fn empty_pix_quota_range_is_rejected() {
1186 let cfg = IncomingSessionPixConfig {
1187 quota_range: 100..=10,
1188 ..Default::default()
1189 };
1190 assert!(validate_incoming_session_pix_config(&cfg).is_err());
1191
1192 let cfg = HoprProtocolConfig {
1193 incoming_session_pix_config: IncomingSessionPixConfig {
1194 quota_range: 100..=10,
1195 ..Default::default()
1196 },
1197 ..Default::default()
1198 };
1199 assert!(cfg.validate().is_err());
1200 }
1201
1202 #[cfg(feature = "serde")]
1203 #[test]
1204 fn pix_configs_are_reachable_from_serialized_config() {
1205 let json = r#"{
1208 "pix": { "num_ssa_parts": 2048 },
1209 "incoming_session_pix_config": { "enforce_pix": true, "max_deposit_wait": "90s" }
1210 }"#;
1211 let cfg: HoprProtocolConfig = serde_json::from_str(json).expect("PIX config must deserialize");
1212
1213 assert_eq!(2048, cfg.pix.num_ssa_parts);
1214 assert_eq!(
1216 PixGlobalConfig::default().ssa_part_size,
1217 cfg.pix.ssa_part_size,
1218 "unspecified PIX fields must fall back to their defaults"
1219 );
1220 assert!(cfg.incoming_session_pix_config.enforce_pix);
1221 assert_eq!(
1222 Duration::from_secs(90),
1223 cfg.incoming_session_pix_config.max_deposit_wait
1224 );
1225 assert_eq!(
1226 IncomingSessionPixConfig::default().quota_range,
1227 cfg.incoming_session_pix_config.quota_range
1228 );
1229
1230 let json = r#"{
1233 "pix": { "max_ssas_per_request": 5 },
1234 "incoming_session_pix_config": { "ssas_per_request": 5 }
1235 }"#;
1236 let cfg: HoprProtocolConfig = serde_json::from_str(json).expect("SSA batch config must deserialize");
1237 assert_eq!(5, cfg.pix.max_ssas_per_request);
1238 assert_eq!(5, cfg.incoming_session_pix_config.ssas_per_request);
1239 cfg.validate().expect("a matched pair of batch knobs must validate");
1240
1241 let json = r#"{
1245 "pix": { "reconstructor": { "max_ack_await_time": "45s", "max_tracked_peers": 500 } }
1246 }"#;
1247 let cfg: HoprProtocolConfig = serde_json::from_str(json).expect("reconstructor config must deserialize");
1248 assert_eq!(Duration::from_secs(45), cfg.pix.reconstructor.max_ack_await_time);
1249 assert_eq!(500, cfg.pix.reconstructor.max_tracked_peers);
1250 assert_eq!(
1251 PixReconstructorConfig::default().unused_verifier_lifetime,
1252 cfg.pix.reconstructor.unused_verifier_lifetime,
1253 "unspecified reconstructor fields must fall back to their defaults"
1254 );
1255 cfg.validate().expect("a narrowed reconstructor must validate");
1256
1257 assert_eq!(
1260 Duration::from_secs(45),
1261 SsaReconstructorConfig::from(cfg.pix.reconstructor).max_ack_await_time
1262 );
1263
1264 let json = r#"{ "pix": { "ssa_part_size": 32 } }"#;
1269 let cfg: HoprProtocolConfig = serde_json::from_str(json).expect("PIX dimensions must deserialize");
1270 assert_eq!(None, cfg.pix.additional_shares, "the field itself stays unset");
1271 assert_eq!(8, cfg.pix.surplus_shares(), "and resolves to ssa_part_size / 4");
1272
1273 let json = r#"{ "pix": { "ssa_part_size": 32, "additional_shares": 30 } }"#;
1274 let cfg: HoprProtocolConfig = serde_json::from_str(json).expect("an explicit surplus must deserialize");
1275 assert_eq!(30, cfg.pix.surplus_shares(), "an explicit surplus is passed through");
1276
1277 let json = r#"{ "pix": { "ssa_part_size": 16, "additional_shares": 20 } }"#;
1278 let cfg: HoprProtocolConfig = serde_json::from_str(json).expect("it parses; validation is what rejects it");
1279 assert!(
1280 cfg.validate().is_err(),
1281 "a surplus above the threshold must be rejected — 20 shares of insurance against 16 of payload"
1282 );
1283 }
1284
1285 #[test]
1298 fn the_surplus_is_bounded_by_the_threshold_it_insures() {
1299 let at_bound = PixGlobalConfig {
1300 ssa_part_size: 64,
1301 additional_shares: Some(64),
1302 ..Default::default()
1303 };
1304 at_bound
1305 .validate()
1306 .expect("a surplus equal to the threshold is allowed — over-insuring a lossy path is a real choice");
1307
1308 let past_bound = PixGlobalConfig {
1309 additional_shares: Some(65),
1310 ..at_bound
1311 };
1312 let errors = past_bound
1313 .validate()
1314 .expect_err("one share past the threshold must be rejected");
1315 assert!(
1316 errors.field_errors().contains_key("__all__"),
1317 "the schema check is what enforces the tighter bound"
1318 );
1319
1320 let past_the_wire_byte = PixGlobalConfig {
1321 additional_shares: Some(300),
1322 ..at_bound
1323 };
1324 let errors = past_the_wire_byte
1325 .validate()
1326 .expect_err("a surplus that cannot fit the PixParams byte must be rejected");
1327 assert!(
1328 errors.field_errors().contains_key("additional_shares"),
1329 "the field range must still fire through the Option, not only the schema check"
1330 );
1331 }
1332
1333 #[test]
1348 fn the_derived_surplus_covers_a_fifth_of_a_polynomial_at_every_threshold() {
1349 for ssa_part_size in 2usize..=255 {
1350 let cfg = PixGlobalConfig {
1351 num_ssa_parts: 8,
1352 ssa_part_size,
1353 ..Default::default()
1354 };
1355 let surplus = cfg.surplus_shares();
1356 let tolerated = surplus as f64 / (ssa_part_size + surplus) as f64;
1357 assert!(
1358 tolerated >= 0.20,
1359 "ssa_part_size {ssa_part_size} derives surplus {surplus}, tolerating only {tolerated:.4} loss"
1360 );
1361 assert!(
1362 surplus > 0,
1363 "ssa_part_size {ssa_part_size} derives no surplus, i.e. no loss tolerance at all"
1364 );
1365 assert!(
1366 cfg.validate().is_ok(),
1367 "a derived surplus must never fail the bound it is derived under"
1368 );
1369 }
1370
1371 for ssa_part_size in [16usize, 32, 48, 64] {
1373 let cfg = PixGlobalConfig {
1374 ssa_part_size,
1375 ..Default::default()
1376 };
1377 let surplus = cfg.surplus_shares();
1378 let tolerated = surplus as f64 / (ssa_part_size + surplus) as f64;
1379 assert!(
1380 (0.19..=0.21).contains(&tolerated),
1381 "ssa_part_size {ssa_part_size} derives surplus {surplus}, tolerating {tolerated:.3} loss"
1382 );
1383 }
1384 }
1385
1386 #[test]
1393 fn ssa_batch_knobs_are_bounded_by_the_shared_ceiling() {
1394 let at_ceiling = PixGlobalConfig {
1396 max_ssas_per_request: MAX_SSA_BATCH_SIZE,
1397 ..Default::default()
1398 };
1399 assert!(
1400 at_ceiling.validate().is_ok(),
1401 "MAX_SSA_BATCH_SIZE itself must be accepted — the range literal has drifted below it"
1402 );
1403 let past_ceiling = PixGlobalConfig {
1404 max_ssas_per_request: MAX_SSA_BATCH_SIZE + 1,
1405 ..Default::default()
1406 };
1407 assert!(
1408 past_ceiling.validate().is_err(),
1409 "above MAX_SSA_BATCH_SIZE must be rejected — the range literal has drifted above it"
1410 );
1411
1412 assert!(
1413 PixGlobalConfig {
1414 max_ssas_per_request: 0,
1415 ..Default::default()
1416 }
1417 .validate()
1418 .is_err(),
1419 "an Entry accepting zero SSAs per request would reject every request"
1420 );
1421
1422 for ssas_per_request in [0, MAX_SSA_BATCH_SIZE + 1] {
1423 let cfg = IncomingSessionPixConfig {
1424 ssas_per_request,
1425 ..Default::default()
1426 };
1427 assert!(
1428 validate_incoming_session_pix_config(&cfg).is_err(),
1429 "ssas_per_request of {ssas_per_request} is outside 1..={MAX_SSA_BATCH_SIZE} and must be rejected"
1430 );
1431
1432 let cfg = HoprProtocolConfig {
1433 incoming_session_pix_config: IncomingSessionPixConfig {
1434 ssas_per_request,
1435 ..Default::default()
1436 },
1437 ..Default::default()
1438 };
1439 assert!(
1440 cfg.validate().is_err(),
1441 "an out-of-range ssas_per_request must fail the whole protocol config"
1442 );
1443 }
1444
1445 assert!(
1446 validate_incoming_session_pix_config(&IncomingSessionPixConfig {
1447 ssas_per_request: MAX_SSA_BATCH_SIZE,
1448 ..Default::default()
1449 })
1450 .is_ok(),
1451 "the ceiling itself must be accepted"
1452 );
1453 }
1454
1455 #[test]
1456 fn egress_backpressure_timeout_rejects_sub_minimum_values() {
1457 assert!(validate_egress_backpressure_timeout(&Duration::ZERO).is_err());
1458 assert!(validate_egress_backpressure_timeout(&Duration::from_micros(500)).is_err());
1459 assert!(validate_egress_backpressure_timeout(&MIN_EGRESS_BACKPRESSURE_TIMEOUT).is_ok());
1460 assert!(validate_egress_backpressure_timeout(&DEFAULT_EGRESS_BACKPRESSURE_TIMEOUT).is_ok());
1461 }
1462
1463 #[test]
1464 fn stream_protocol_config_default_is_valid() {
1465 assert!(StreamProtocolConfig::default().validate().is_ok());
1466 }
1467
1468 #[test]
1469 fn test_valid_domains_for_looks_like_a_domain() {
1470 assert!(looks_like_domain("localhost"));
1471 assert!(looks_like_domain("hoprnet.org"));
1472 assert!(looks_like_domain("hub.hoprnet.org"));
1473 }
1474
1475 #[test]
1476 fn test_valid_domains_for_does_not_look_like_a_domain() {
1477 assert!(!looks_like_domain(".org"));
1478 assert!(!looks_like_domain("-hoprnet-.org"));
1479 }
1480
1481 #[test]
1482 fn test_valid_domains_should_be_reachable() {
1483 assert!(!is_reachable_domain("google.com"));
1484 }
1485
1486 #[test]
1487 fn test_verify_valid_ip4_addresses() {
1488 assert!(validate_ipv4_address("1.1.1.1").is_ok());
1489 assert!(validate_ipv4_address("1.255.1.1").is_ok());
1490 assert!(validate_ipv4_address("187.1.1.255").is_ok());
1491 assert!(validate_ipv4_address("127.0.0.1").is_ok());
1492 }
1493
1494 #[test]
1495 fn test_verify_invalid_ip4_addresses() {
1496 assert!(validate_ipv4_address("1.256.1.1").is_err());
1497 assert!(validate_ipv4_address("-1.1.1.255").is_err());
1498 assert!(validate_ipv4_address("127.0.0.256").is_err());
1499 assert!(validate_ipv4_address("1").is_err());
1500 assert!(validate_ipv4_address("1.1").is_err());
1501 assert!(validate_ipv4_address("1.1.1").is_err());
1502 assert!(validate_ipv4_address("1.1.1.1.1").is_err());
1503 }
1504
1505 #[test]
1506 fn test_verify_valid_dns_addresses() {
1507 assert!(validate_dns_address("localhost").is_ok());
1508 assert!(validate_dns_address("google.com").is_ok());
1509 assert!(validate_dns_address("hub.hoprnet.org").is_ok());
1510 }
1511
1512 #[test]
1513 fn test_verify_invalid_dns_addresses() {
1514 assert!(validate_dns_address("-hoprnet-.org").is_err());
1515 }
1516
1517 #[test]
1518 fn test_multiaddress_on_dappnode_default() {
1519 temp_env::with_var("DAPPNODE", Some("true"), || {
1520 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
1521 });
1522 }
1523
1524 #[cfg(feature = "p2p-announce-quic")]
1525 #[test]
1526 fn test_multiaddress_on_non_dappnode_default() {
1527 temp_env::with_vars([("DAPPNODE", Some("false")), ("HOPRD_NAT", Some("false"))], || {
1528 assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
1529 });
1530 }
1531
1532 #[cfg(not(feature = "p2p-announce-quic"))]
1533 #[test]
1534 fn test_multiaddress_on_non_dappnode_default() {
1535 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
1536 }
1537
1538 #[test]
1539 fn test_multiaddress_on_non_dappnode_uses_nat() {
1540 temp_env::with_var("HOPRD_NAT", Some("true"), || {
1541 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
1542 });
1543 }
1544
1545 #[cfg(feature = "p2p-announce-quic")]
1546 #[test]
1547 fn test_multiaddress_on_non_dappnode_not_uses_nat() {
1548 temp_env::with_var("HOPRD_NAT", Some("false"), || {
1549 assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
1550 });
1551 }
1552
1553 #[cfg(not(feature = "p2p-announce-quic"))]
1554 #[test]
1555 fn test_multiaddress_on_non_dappnode_not_uses_nat() {
1556 temp_env::with_var("HOPRD_NAT", Some("false"), || {
1557 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
1558 });
1559 }
1560
1561 #[cfg(feature = "p2p-announce-quic")]
1562 #[test]
1563 fn test_multiaddress_on_dappnode_not_uses_nat() {
1564 temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
1565 assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
1566 });
1567 }
1568
1569 #[cfg(not(feature = "p2p-announce-quic"))]
1570 #[test]
1571 fn test_multiaddress_on_dappnode_not_uses_nat() {
1572 temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
1573 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
1574 });
1575 }
1576
1577 #[test]
1580 fn host_config_parses_ipv4_address() {
1581 let cfg = HostConfig::from_str("1.2.3.4:9091").unwrap();
1582 insta::assert_debug_snapshot!(cfg);
1583 }
1584
1585 #[test]
1586 fn host_config_parses_domain() {
1587 let cfg = HostConfig::from_str("example.com:443").unwrap();
1588 insta::assert_debug_snapshot!(cfg);
1589 }
1590
1591 #[test]
1592 fn host_config_rejects_missing_port() {
1593 assert!(HostConfig::from_str("1.2.3.4").is_err());
1594 }
1595
1596 #[test]
1597 fn host_config_rejects_invalid_port() {
1598 assert!(HostConfig::from_str("1.2.3.4:abc").is_err());
1599 }
1600
1601 #[test]
1602 fn host_config_rejects_invalid_host() {
1603 assert!(HostConfig::from_str("-invalid-.com:80").is_err());
1604 }
1605
1606 #[test]
1607 fn host_config_display_roundtrip() {
1608 let cfg = HostConfig {
1609 address: HostType::IPv4("10.0.0.1".into()),
1610 port: 8080,
1611 };
1612 insta::assert_yaml_snapshot!(cfg.to_string());
1613 }
1614
1615 #[test]
1618 fn multiaddr_from_ipv4_host_config() {
1619 let cfg = HostConfig {
1620 address: HostType::IPv4("1.2.3.4".into()),
1621 port: 9091,
1622 };
1623 let addr = Multiaddr::try_from(&cfg).unwrap();
1624 insta::assert_yaml_snapshot!(addr.to_string());
1625 }
1626
1627 #[test]
1628 fn multiaddr_from_domain_host_config() {
1629 let cfg = HostConfig {
1630 address: HostType::Domain("example.com".into()),
1631 port: 443,
1632 };
1633 let addr = Multiaddr::try_from(&cfg).unwrap();
1634 insta::assert_yaml_snapshot!(addr.to_string());
1635 }
1636
1637 #[test]
1640 fn session_global_config_default_is_valid() {
1641 let cfg = SessionGlobalConfig::default();
1642 assert!(cfg.validate().is_ok());
1643 }
1644
1645 #[test]
1646 fn session_global_config_too_low_idle_timeout_is_rejected() {
1647 let cfg = SessionGlobalConfig {
1648 idle_timeout: Duration::from_millis(100),
1649 ..Default::default()
1650 };
1651 assert!(cfg.validate().is_err());
1652 }
1653
1654 #[test]
1655 fn session_global_config_too_many_retries_is_rejected() {
1656 let cfg = SessionGlobalConfig {
1657 establish_max_retries: 21,
1658 ..Default::default()
1659 };
1660 assert!(cfg.validate().is_err());
1661 }
1662
1663 #[test]
1664 fn stream_protocol_config_default_has_expected_values() {
1665 let cfg = StreamProtocolConfig::default();
1666 assert_eq!(cfg.per_peer_channel_capacity, DEFAULT_PER_PEER_CHANNEL_CAPACITY);
1667 assert_eq!(cfg.stream_open_timeout, DEFAULT_STREAM_OPEN_TIMEOUT);
1668 assert_eq!(
1669 cfg.frame_writer_backpressure_bytes,
1670 DEFAULT_FRAME_WRITER_BACKPRESSURE_BYTES
1671 );
1672 cfg.validate().expect("default StreamProtocolConfig must be valid");
1673 }
1674
1675 #[test]
1676 fn stream_protocol_config_zero_capacity_is_rejected() {
1677 let cfg = StreamProtocolConfig {
1678 per_peer_channel_capacity: 0,
1679 ..Default::default()
1680 };
1681 assert!(cfg.validate().is_err());
1682 }
1683
1684 #[test]
1685 fn stream_protocol_config_zero_backpressure_bytes_is_rejected() {
1686 let cfg = StreamProtocolConfig {
1687 frame_writer_backpressure_bytes: 0,
1688 ..Default::default()
1689 };
1690 assert!(cfg.validate().is_err());
1691 }
1692
1693 #[test]
1694 fn stream_protocol_config_zero_stream_open_timeout_is_rejected() {
1695 let cfg = StreamProtocolConfig {
1696 stream_open_timeout: Duration::ZERO,
1697 ..Default::default()
1698 };
1699 assert!(cfg.validate().is_err());
1700 }
1701
1702 #[test]
1703 fn stream_protocol_config_zero_backpressure_timeout_is_rejected() {
1704 let cfg = StreamProtocolConfig {
1708 egress_backpressure_timeout: Duration::ZERO,
1709 ..Default::default()
1710 };
1711 assert!(cfg.validate().is_err());
1712 }
1713}