1use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9use satd_events_proto::v1 as pb;
10use satd_events_proto::v1::node_event_stream_client::NodeEventStreamClient;
11use tokio::sync::mpsc;
12use tokio_stream::wrappers::ReceiverStream;
13use tonic::metadata::{Ascii, MetadataMap, MetadataValue};
14use tonic::transport::{Channel, Endpoint};
15
16use crate::compat::{self, Compat};
17use crate::error::StreamError;
18use crate::event::{Cursor, Event};
19
20const CONTROL_BUFFER: usize = 16;
22
23pub struct Categories;
28
29impl Categories {
30 pub const ALL: u32 = 0;
32 pub const MEMPOOL: u32 = 1;
34 pub const CHAIN: u32 = 2;
36 pub const HEARTBEAT: u32 = 4;
38 pub const TWEAKS: u32 = 8;
45 pub const STATUS: u32 = 16;
52}
53
54#[derive(Debug, Clone, Default)]
56pub struct SubscribeOptions {
57 pub categories: u32,
59 pub from_cursor: Option<Cursor>,
62 pub since_seq: Option<u64>,
65 pub tweak_dust_limit: Option<u64>,
70 pub tweaks_only: bool,
74 pub mempool_tweaks: bool,
83 pub tweak_outputs: bool,
93 pub tweak_unspent_only: bool,
109}
110
111impl SubscribeOptions {
112 #[allow(clippy::needless_update)]
116 fn into_request(self) -> pb::SubscribeRequest {
117 pb::SubscribeRequest {
118 categories: self.categories,
119 since_seq: self.since_seq,
120 from_cursor: self.from_cursor,
121 tweak_dust_limit: self.tweak_dust_limit,
122 tweaks_only: self.tweaks_only.then_some(true),
125 mempool_tweaks: self.mempool_tweaks.then_some(true),
126 tweak_outputs: self.tweak_outputs.then_some(true),
127 tweak_unspent_only: self.tweak_unspent_only.then_some(true),
128 ..Default::default()
129 }
130 }
131}
132
133pub const MAX_SP_LABELS_PER_TARGET: usize = 16;
138
139pub const MAX_SP_TARGETS_PER_CONNECTION: usize = 16;
145
146#[derive(Clone)]
156pub struct SilentPaymentTarget {
157 pub scan_secret: [u8; 32],
159 pub spend_pubkey: [u8; 33],
161 pub labels: Vec<u32>,
163}
164
165impl core::fmt::Debug for SilentPaymentTarget {
166 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
167 f.debug_struct("SilentPaymentTarget")
168 .field("scan_secret", &"<redacted>")
169 .field("spend_pubkey", &hex_lower(&self.spend_pubkey))
170 .field("labels", &self.labels)
171 .finish()
172 }
173}
174
175impl Drop for SilentPaymentTarget {
176 fn drop(&mut self) {
183 use zeroize::Zeroize;
184 self.scan_secret.zeroize();
185 }
186}
187
188impl SilentPaymentTarget {
189 pub(crate) fn to_proto(&self) -> pb::SilentPaymentTarget {
190 pb::SilentPaymentTarget {
191 scan_secret: self.scan_secret.to_vec(),
192 spend_pubkey: self.spend_pubkey.to_vec(),
193 labels: self.labels.clone(),
194 }
195 }
196
197 #[cfg(feature = "bitcoin")]
202 pub fn scan_pubkey(&self) -> Result<[u8; 33], StreamError> {
203 use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
204 let sk = SecretKey::from_slice(&self.scan_secret)
205 .map_err(|_| StreamError::InvalidArgument("scan_secret is not a valid scalar".into()))?;
206 let secp = Secp256k1::signing_only();
207 Ok(PublicKey::from_secret_key(&secp, &sk).serialize())
208 }
209
210 #[cfg(feature = "bitcoin")]
218 pub fn validate(&self) -> Result<[u8; 33], StreamError> {
219 let id = self.scan_pubkey()?; bitcoin::secp256k1::PublicKey::from_slice(&self.spend_pubkey)
221 .map_err(|_| StreamError::InvalidArgument("spend_pubkey is not a valid point".into()))?;
222 let mut labels = self.labels.clone();
226 labels.sort_unstable();
227 labels.dedup();
228 if labels.len() > MAX_SP_LABELS_PER_TARGET {
229 return Err(StreamError::InvalidArgument(format!(
230 "too many silent-payment labels: {} distinct (max {MAX_SP_LABELS_PER_TARGET})",
231 labels.len()
232 )));
233 }
234 Ok(id)
235 }
236}
237
238fn hex_lower(bytes: &[u8]) -> String {
240 let mut s = String::with_capacity(bytes.len() * 2);
241 for b in bytes {
242 s.push_str(&format!("{b:02x}"));
243 }
244 s
245}
246
247pub struct EventStream {
253 inner: tonic::Streaming<pb::NodeEvent>,
254 last_cursor: Option<Cursor>,
255 schema_checked: bool,
257}
258
259impl EventStream {
260 pub async fn message(&mut self) -> Result<Option<Event>, StreamError> {
263 match self.inner.message().await {
264 Ok(Some(ev)) => {
265 if !self.schema_checked {
269 self.schema_checked = true;
270 if ev.schema_version != satd_events_proto::SCHEMA_VERSION {
271 return Err(StreamError::SchemaMismatch {
272 node: ev.schema_version,
273 sdk: satd_events_proto::SCHEMA_VERSION,
274 });
275 }
276 }
277 if let Some(c) = ev.cursor {
278 self.last_cursor = Some(c);
279 }
280 Ok(Some(Event::from(ev)))
281 }
282 Ok(None) => Ok(None),
283 Err(status) => Err(StreamError::from_status(status)),
284 }
285 }
286
287 pub fn cursor(&self) -> Option<&Cursor> {
290 self.last_cursor.as_ref()
291 }
292
293 fn new(inner: tonic::Streaming<pb::NodeEvent>) -> Self {
294 EventStream { inner, last_cursor: None, schema_checked: false }
295 }
296}
297
298#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
301pub enum AutoClose {
302 #[default]
304 Never,
305 AtDepth(u32),
307}
308
309impl AutoClose {
310 fn depth(self) -> u32 {
311 match self {
312 AutoClose::Never => 0,
313 AutoClose::AtDepth(d) => d,
314 }
315 }
316}
317
318pub struct WatchHandle {
325 tx: mpsc::Sender<pb::SubscribeControl>,
326}
327
328impl WatchHandle {
329 pub async fn send_control(&self, ctrl: pb::SubscribeControl) -> Result<(), StreamError> {
331 self.tx.send(ctrl).await.map_err(|_| StreamError::ControlClosed)
332 }
333
334 async fn send_msg(&self, msg: pb::subscribe_control::Msg) -> Result<(), StreamError> {
335 self.send_control(pb::SubscribeControl { msg: Some(msg) }).await
336 }
337
338 pub async fn add_scripts(
343 &self,
344 items: impl IntoIterator<Item = ([u8; 32], Option<u64>)>,
345 ) -> Result<(), StreamError> {
346 let items: Vec<_> = items.into_iter().collect();
347 if items.is_empty() {
348 return Ok(());
349 }
350 let scripthashes = items.iter().map(|(h, _)| h.to_vec()).collect();
351 let min_values = if items.iter().any(|(_, f)| f.is_some()) {
355 items.iter().map(|(_, f)| f.unwrap_or(0)).collect()
356 } else {
357 Vec::new()
358 };
359 self.send_msg(pb::subscribe_control::Msg::AddScripts(pb::AddScripts {
360 scripthashes,
361 min_values,
362 }))
363 .await
364 }
365
366 pub async fn remove_scripts(
368 &self,
369 scripthashes: impl IntoIterator<Item = [u8; 32]>,
370 ) -> Result<(), StreamError> {
371 let scripthashes: Vec<Vec<u8>> = scripthashes.into_iter().map(|h| h.to_vec()).collect();
372 if scripthashes.is_empty() {
373 return Ok(());
374 }
375 self.send_msg(pb::subscribe_control::Msg::RemoveScripts(pb::RemoveScripts {
376 scripthashes,
377 }))
378 .await
379 }
380
381 pub async fn add_outpoints(
383 &self,
384 outpoints: impl IntoIterator<Item = ([u8; 32], u32)>,
385 ) -> Result<(), StreamError> {
386 let outpoints: Vec<pb::Outpoint> = outpoints
387 .into_iter()
388 .map(|(txid, vout)| pb::Outpoint { txid: txid.to_vec(), vout })
389 .collect();
390 if outpoints.is_empty() {
391 return Ok(());
392 }
393 self.send_msg(pb::subscribe_control::Msg::AddOutpoints(pb::AddOutpoints { outpoints }))
394 .await
395 }
396
397 pub async fn remove_outpoints(
399 &self,
400 outpoints: impl IntoIterator<Item = ([u8; 32], u32)>,
401 ) -> Result<(), StreamError> {
402 let outpoints: Vec<pb::Outpoint> = outpoints
403 .into_iter()
404 .map(|(txid, vout)| pb::Outpoint { txid: txid.to_vec(), vout })
405 .collect();
406 if outpoints.is_empty() {
407 return Ok(());
408 }
409 self.send_msg(pb::subscribe_control::Msg::RemoveOutpoints(pb::RemoveOutpoints {
410 outpoints,
411 }))
412 .await
413 }
414
415 pub async fn add_tx_lifecycle(
419 &self,
420 txids: impl IntoIterator<Item = [u8; 32]>,
421 auto_close: AutoClose,
422 ) -> Result<(), StreamError> {
423 let txids: Vec<Vec<u8>> = txids.into_iter().map(|t| t.to_vec()).collect();
424 if txids.is_empty() {
425 return Ok(());
426 }
427 self.send_msg(pb::subscribe_control::Msg::AddTransactions(pb::AddTransactions {
428 txids,
429 min_depths: Vec::new(),
430 auto_close_depth: auto_close.depth(),
431 }))
432 .await
433 }
434
435 pub async fn remove_tx_lifecycle(
437 &self,
438 txids: impl IntoIterator<Item = [u8; 32]>,
439 ) -> Result<(), StreamError> {
440 let txids: Vec<Vec<u8>> = txids.into_iter().map(|t| t.to_vec()).collect();
441 if txids.is_empty() {
442 return Ok(());
443 }
444 self.send_msg(pb::subscribe_control::Msg::RemoveTransactions(pb::RemoveTransactions {
445 txids,
446 min_depths: Vec::new(),
447 }))
448 .await
449 }
450
451 pub async fn add_depth_alarms(
460 &self,
461 txids: impl IntoIterator<Item = [u8; 32]>,
462 depths: impl IntoIterator<Item = u32>,
463 ) -> Result<(), StreamError> {
464 let txids: Vec<Vec<u8>> = txids.into_iter().map(|t| t.to_vec()).collect();
465 let min_depths: Vec<u32> = depths.into_iter().filter(|d| *d >= 1).collect();
466 if txids.is_empty() || min_depths.is_empty() {
467 return Ok(());
468 }
469 self.send_msg(pb::subscribe_control::Msg::AddTransactions(pb::AddTransactions {
470 txids,
471 min_depths,
472 auto_close_depth: 0,
473 }))
474 .await
475 }
476
477 pub async fn remove_depth_alarms(
482 &self,
483 txids: impl IntoIterator<Item = [u8; 32]>,
484 depths: impl IntoIterator<Item = u32>,
485 ) -> Result<(), StreamError> {
486 let txids: Vec<Vec<u8>> = txids.into_iter().map(|t| t.to_vec()).collect();
487 let min_depths: Vec<u32> = depths.into_iter().filter(|d| *d >= 1).collect();
488 if txids.is_empty() || min_depths.is_empty() {
489 return Ok(());
490 }
491 self.send_msg(pb::subscribe_control::Msg::RemoveTransactions(pb::RemoveTransactions {
492 txids,
493 min_depths,
494 }))
495 .await
496 }
497
498 pub async fn add_descriptor(
505 &self,
506 descriptor: impl Into<String>,
507 gap_limit: u32,
508 start: u32,
509 ) -> Result<(), StreamError> {
510 self.send_msg(pb::subscribe_control::Msg::AddDescriptor(pb::AddDescriptor {
511 descriptor: descriptor.into(),
512 gap_limit,
513 start,
514 }))
515 .await
516 }
517
518 pub async fn remove_descriptor(
524 &self,
525 descriptor: impl Into<String>,
526 ) -> Result<(), StreamError> {
527 self.send_msg(pb::subscribe_control::Msg::RemoveDescriptor(pb::RemoveDescriptor {
528 descriptor: descriptor.into(),
529 }))
530 .await
531 }
532
533 pub async fn add_script_prefixes(
543 &self,
544 prefixes: impl IntoIterator<Item = (Vec<u8>, u32)>,
545 ) -> Result<(), StreamError> {
546 let prefixes: Vec<pb::ScriptPrefix> = prefixes
547 .into_iter()
548 .map(|(prefix, bits)| validate_prefix(prefix, bits))
549 .collect::<Result<_, _>>()?;
550 if prefixes.is_empty() {
551 return Ok(());
552 }
553 self.send_msg(pb::subscribe_control::Msg::AddScriptPrefixes(pb::AddScriptPrefixes {
554 prefixes,
555 }))
556 .await
557 }
558
559 pub async fn remove_script_prefixes(
561 &self,
562 prefixes: impl IntoIterator<Item = (Vec<u8>, u32)>,
563 ) -> Result<(), StreamError> {
564 let prefixes: Vec<pb::ScriptPrefix> = prefixes
565 .into_iter()
566 .map(|(prefix, bits)| validate_prefix(prefix, bits))
567 .collect::<Result<_, _>>()?;
568 if prefixes.is_empty() {
569 return Ok(());
570 }
571 self.send_msg(pb::subscribe_control::Msg::RemoveScriptPrefixes(
572 pb::RemoveScriptPrefixes { prefixes },
573 ))
574 .await
575 }
576
577 pub async fn add_silent_payments(
594 &self,
595 targets: impl IntoIterator<Item = SilentPaymentTarget>,
596 ) -> Result<(), StreamError> {
597 let targets: Vec<SilentPaymentTarget> = targets.into_iter().collect();
598 #[cfg(feature = "bitcoin")]
599 for t in &targets {
600 t.validate()?;
601 }
602 let targets: Vec<pb::SilentPaymentTarget> = targets.iter().map(|t| t.to_proto()).collect();
603 if targets.is_empty() {
604 return Ok(());
605 }
606 self.send_msg(pb::subscribe_control::Msg::AddSilentPayments(
607 pb::AddSilentPayments { targets },
608 ))
609 .await
610 }
611
612 pub async fn remove_silent_payments(
615 &self,
616 scan_pubkeys: impl IntoIterator<Item = [u8; 33]>,
617 ) -> Result<(), StreamError> {
618 let scan_pubkeys: Vec<Vec<u8>> = scan_pubkeys.into_iter().map(|k| k.to_vec()).collect();
619 if scan_pubkeys.is_empty() {
620 return Ok(());
621 }
622 self.send_msg(pb::subscribe_control::Msg::RemoveSilentPayments(
623 pb::RemoveSilentPayments { scan_pubkeys },
624 ))
625 .await
626 }
627
628 pub async fn set_categories(&self, categories: u32) -> Result<(), StreamError> {
631 self.send_msg(pb::subscribe_control::Msg::SetCategories(pb::SetCategories {
632 categories,
633 }))
634 .await
635 }
636
637 pub async fn set_watch_options(&self, include_raw_tx: bool) -> Result<(), StreamError> {
649 self.send_msg(pb::subscribe_control::Msg::SetWatchOptions(
650 pb::SetWatchOptions { include_raw_tx },
651 ))
652 .await
653 }
654
655 pub async fn set_cursor(&self, cursor: Cursor) -> Result<(), StreamError> {
675 self.send_msg(pb::subscribe_control::Msg::SetCursor(pb::SetCursor {
676 cursor: Some(cursor),
677 }))
678 .await
679 }
680
681 pub async fn rescan(&self, from_height: u32, to_height: u32) -> Result<(), StreamError> {
693 self.send_msg(pb::subscribe_control::Msg::RescanBlocks(pb::RescanBlocks {
694 from_height,
695 to_height,
696 }))
697 .await
698 }
699}
700
701pub(crate) const MAX_PREFIX_BITS: u32 = 32;
710
711pub(crate) fn validate_prefix(
713 prefix: Vec<u8>,
714 bits: u32,
715) -> Result<pb::ScriptPrefix, StreamError> {
716 if !(1..=MAX_PREFIX_BITS).contains(&bits) {
717 return Err(StreamError::InvalidArgument(format!(
718 "prefix bits {bits} out of range 1..={MAX_PREFIX_BITS}"
719 )));
720 }
721 let want = bits.div_ceil(8) as usize;
722 if prefix.len() != want {
723 return Err(StreamError::InvalidArgument(format!(
724 "prefix for {bits} bits must be {want} bytes, got {}",
725 prefix.len()
726 )));
727 }
728 Ok(pb::ScriptPrefix { prefix, bits })
729}
730
731#[cfg_attr(not(feature = "tls"), allow(dead_code))]
742fn endpoint_is_https(endpoint: &str) -> bool {
743 endpoint
744 .split_once("://")
745 .is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("https"))
746}
747
748#[cfg(feature = "tls")]
760fn endpoint_is_encrypted(endpoint: &str) -> bool {
761 endpoint_is_https(endpoint)
762}
763
764#[cfg(not(feature = "tls"))]
765fn endpoint_is_encrypted(_endpoint: &str) -> bool {
766 false
767}
768
769fn redact_endpoint_userinfo(endpoint: &str) -> String {
774 let Some((scheme, rest)) = endpoint.split_once("://") else {
775 return match endpoint.rsplit_once('@') {
777 Some((_, host)) => format!("***@{host}"),
778 None => endpoint.to_string(),
779 };
780 };
781 let (authority, path) = match rest.find('/') {
784 Some(i) => rest.split_at(i),
785 None => (rest, ""),
786 };
787 match authority.rsplit_once('@') {
788 Some((_, host)) => format!("{scheme}://***@{host}{path}"),
789 None => endpoint.to_string(),
790 }
791}
792
793#[cfg(feature = "tls")]
796fn insecure_credential_reason(endpoint: &str) -> String {
797 redact_endpoint_userinfo(endpoint)
798}
799
800#[cfg(not(feature = "tls"))]
801fn insecure_credential_reason(endpoint: &str) -> String {
802 format!(
803 "{} — this build of satd-events-client has the `tls` feature disabled, \
804 so no endpoint is encrypted here, https:// included",
805 redact_endpoint_userinfo(endpoint)
806 )
807}
808
809#[cfg(feature = "tls")]
813#[derive(Clone, Default)]
814struct TlsSettings {
815 ca_pem: Option<Vec<u8>>,
818 identity: Option<(Vec<u8>, Vec<u8>)>,
820 domain: Option<String>,
822}
823
824#[derive(Clone)]
829pub struct StreamClientBuilder {
830 endpoint: String,
831 token: Option<String>,
832 allow_insecure_token: bool,
835 keepalive: Option<(Duration, Duration)>,
836 allow_old_node: bool,
837 #[cfg(feature = "tls")]
838 tls: Option<TlsSettings>,
839}
840
841impl std::fmt::Debug for StreamClientBuilder {
842 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
843 let mut d = f.debug_struct("StreamClientBuilder");
844 d.field("endpoint", &self.endpoint)
845 .field("token", &self.token.as_ref().map(|_| "<redacted>"))
846 .field("allow_insecure_token", &self.allow_insecure_token)
849 .field("keepalive", &self.keepalive)
850 .field("allow_old_node", &self.allow_old_node);
851 #[cfg(feature = "tls")]
852 d.field("tls", &self.tls.is_some());
853 d.finish()
854 }
855}
856
857impl StreamClientBuilder {
858 pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
871 self.token = Some(token.into());
872 self.allow_insecure_token = false;
873 self
874 }
875
876 pub fn insecure_bearer_token(mut self, token: impl Into<String>) -> Self {
889 self.token = Some(token.into());
890 self.allow_insecure_token = true;
891 self
892 }
893
894 pub fn keepalive(mut self, interval_secs: u64, timeout_secs: u64) -> Self {
898 self.keepalive =
899 Some((Duration::from_secs(interval_secs), Duration::from_secs(timeout_secs)));
900 self
901 }
902
903 pub fn keepalive_default(self) -> Self {
905 self.keepalive(30, 20)
906 }
907
908 pub fn allow_old_node(mut self) -> Self {
920 self.allow_old_node = true;
921 self
922 }
923
924 #[cfg(feature = "tls")]
930 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
931 pub fn tls(mut self) -> Self {
932 self.tls.get_or_insert_with(TlsSettings::default);
933 self
934 }
935
936 #[cfg(feature = "tls")]
940 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
941 pub fn tls_ca_pem(mut self, pem: impl Into<Vec<u8>>) -> Self {
942 self.tls.get_or_insert_with(TlsSettings::default).ca_pem = Some(pem.into());
943 self
944 }
945
946 #[cfg(feature = "tls")]
953 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
954 pub fn tls_client_identity(
955 mut self,
956 cert_pem: impl Into<Vec<u8>>,
957 key_pem: impl Into<Vec<u8>>,
958 ) -> Self {
959 self.tls.get_or_insert_with(TlsSettings::default).identity =
960 Some((cert_pem.into(), key_pem.into()));
961 self
962 }
963
964 #[cfg(feature = "tls")]
968 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
969 pub fn tls_domain(mut self, domain: impl Into<String>) -> Self {
970 self.tls.get_or_insert_with(TlsSettings::default).domain = Some(domain.into());
971 self
972 }
973
974 pub async fn connect(self) -> Result<StreamClient, StreamError> {
976 let has_token = self.token.is_some();
979 let auth = match self.token {
980 Some(t) => Some(
981 format!("Bearer {t}")
982 .parse::<MetadataValue<Ascii>>()
983 .map_err(|_| StreamError::InvalidToken)?,
984 ),
985 None => None,
986 };
987
988 #[cfg(feature = "tls")]
994 if self.tls.is_some() && !endpoint_is_https(&self.endpoint) {
995 return Err(StreamError::InvalidEndpoint(
996 "TLS was requested (tls / tls_ca_pem / tls_client_identity / tls_domain) but \
997 the endpoint scheme is not https:// — refusing to connect in cleartext; use \
998 an https:// endpoint"
999 .to_string(),
1000 ));
1001 }
1002
1003 if has_token && !self.allow_insecure_token && !endpoint_is_encrypted(&self.endpoint) {
1024 return Err(StreamError::InsecureCredential(insecure_credential_reason(
1025 &self.endpoint,
1026 )));
1027 }
1028
1029 let mut endpoint = Endpoint::from_shared(self.endpoint)
1030 .map_err(|e| StreamError::InvalidEndpoint(e.to_string()))?;
1031 if let Some((interval, timeout)) = self.keepalive {
1032 endpoint = endpoint
1033 .http2_keep_alive_interval(interval)
1034 .keep_alive_timeout(timeout)
1035 .keep_alive_while_idle(true);
1036 }
1037 #[cfg(feature = "tls")]
1038 if let Some(tls) = self.tls {
1039 ensure_crypto_provider();
1040 let mut cfg = tonic::transport::ClientTlsConfig::new();
1041 cfg = match tls.ca_pem {
1044 Some(ca) => cfg.ca_certificate(tonic::transport::Certificate::from_pem(ca)),
1045 None => cfg.with_webpki_roots(),
1046 };
1047 if let Some((cert, key)) = tls.identity {
1048 cfg = cfg.identity(tonic::transport::Identity::from_pem(cert, key));
1049 }
1050 if let Some(domain) = tls.domain {
1051 cfg = cfg.domain_name(domain);
1052 }
1053 endpoint = endpoint.tls_config(cfg)?;
1054 }
1055
1056 let channel = endpoint.connect().await?;
1057 Ok(StreamClient {
1058 inner: NodeEventStreamClient::new(channel),
1059 auth,
1060 allow_old_node: self.allow_old_node,
1061 compat: Arc::default(),
1062 })
1063 }
1064}
1065
1066#[cfg(feature = "tls")]
1073fn ensure_crypto_provider() {
1074 use std::sync::Once;
1075 static ONCE: Once = Once::new();
1076 ONCE.call_once(|| {
1077 let _ = tokio_rustls::rustls::crypto::ring::default_provider().install_default();
1078 });
1079}
1080
1081#[derive(Clone)]
1085pub struct StreamClient {
1086 inner: NodeEventStreamClient<Channel>,
1087 auth: Option<MetadataValue<Ascii>>,
1088 allow_old_node: bool,
1089 compat: Arc<Mutex<CompatState>>,
1093}
1094
1095#[derive(Debug, Default)]
1097struct CompatState {
1098 node_version: Option<String>,
1100 warned_for: Option<Option<String>>,
1103}
1104
1105impl std::fmt::Debug for StreamClient {
1106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1107 f.debug_struct("StreamClient")
1108 .field("auth", &self.auth.as_ref().map(|_| "<redacted>"))
1109 .finish_non_exhaustive()
1110 }
1111}
1112
1113impl StreamClient {
1114 pub fn builder(endpoint: impl Into<String>) -> StreamClientBuilder {
1117 StreamClientBuilder {
1118 endpoint: endpoint.into(),
1119 token: None,
1120 allow_insecure_token: false,
1121 keepalive: None,
1122 allow_old_node: false,
1123 #[cfg(feature = "tls")]
1124 tls: None,
1125 }
1126 }
1127
1128 pub fn node_version(&self) -> Option<String> {
1134 self.compat.lock().unwrap_or_else(|e| e.into_inner()).node_version.clone()
1135 }
1136
1137 fn check_compat(&self, md: &MetadataMap) -> Result<(), StreamError> {
1140 let (raw, node) = compat::node_version_from(md);
1141 let schema = compat::schema_from(md);
1142 let sdk = compat::parse(compat::SDK_VERSION).expect("the crate version parses");
1143
1144 let warn_for = {
1148 let mut state = self.compat.lock().unwrap_or_else(|e| e.into_inner());
1149 state.node_version = raw.clone();
1150
1151 if schema != satd_events_proto::SCHEMA_VERSION {
1152 return Err(StreamError::SchemaMismatch {
1153 node: schema,
1154 sdk: satd_events_proto::SCHEMA_VERSION,
1155 });
1156 }
1157
1158 let warn = match compat::classify(sdk, node) {
1159 Compat::Ok => false,
1160 Compat::OneBehind => true,
1161 Compat::TooOld if self.allow_old_node => true,
1162 Compat::TooOld => {
1163 return Err(StreamError::NodeTooOld {
1164 node: compat::display_node_version(raw.as_deref()),
1165 sdk: compat::SDK_VERSION,
1166 });
1167 }
1168 };
1169 if warn && state.warned_for.as_ref() != Some(&raw) {
1172 state.warned_for = Some(raw.clone());
1173 Some(compat::display_node_version(raw.as_deref()))
1174 } else {
1175 None
1176 }
1177 };
1178
1179 if let Some(shown) = warn_for {
1180 tracing::warn!(
1181 target: "satd_events_client::compat",
1182 node_version = %shown,
1183 sdk_version = compat::SDK_VERSION,
1184 "satd node {} is older than this SDK ({}); features added after the node's \
1185 release are unavailable and request fields it does not recognise are ignored. \
1186 Upgrade the node — connections are refused once it is two minor versions behind.",
1187 shown,
1188 compat::SDK_VERSION,
1189 );
1190 }
1191 Ok(())
1192 }
1193
1194 fn authed<T>(&self, msg: T) -> tonic::Request<T> {
1196 let mut req = tonic::Request::new(msg);
1197 if let Some(value) = &self.auth {
1198 req.metadata_mut().insert("authorization", value.clone());
1199 }
1200 req
1201 }
1202
1203 pub async fn subscribe(
1206 &mut self,
1207 opts: SubscribeOptions,
1208 ) -> Result<EventStream, StreamError> {
1209 let req = self.authed(opts.into_request());
1210 let resp = self.inner.subscribe(req).await?;
1211 self.check_compat(resp.metadata())?;
1212 Ok(EventStream::new(resp.into_inner()))
1213 }
1214
1215 pub fn resilient_subscribe(
1222 &self,
1223 opts: SubscribeOptions,
1224 config: crate::ResilientConfig,
1225 ) -> crate::ResilientSubscription {
1226 crate::ResilientSubscription::new(self.clone(), opts, config)
1227 }
1228
1229 pub fn resilient_watch(&self, config: crate::ResilientWatchConfig) -> crate::ResilientWatch {
1235 crate::ResilientWatch::new(self.clone(), config)
1236 }
1237
1238 pub async fn watch(&mut self) -> Result<(WatchHandle, EventStream), StreamError> {
1241 let (tx, rx) = mpsc::channel::<pb::SubscribeControl>(CONTROL_BUFFER);
1242 let req = self.authed(ReceiverStream::new(rx));
1243 let resp = self.inner.watch(req).await?;
1244 self.check_compat(resp.metadata())?;
1245 Ok((WatchHandle { tx }, EventStream::new(resp.into_inner())))
1246 }
1247}
1248
1249#[cfg(test)]
1250impl WatchHandle {
1251 pub(crate) fn for_test() -> (Self, mpsc::Receiver<pb::SubscribeControl>) {
1255 let (tx, rx) = mpsc::channel(64);
1256 (WatchHandle { tx }, rx)
1257 }
1258}
1259
1260#[cfg(test)]
1261impl StreamClient {
1262 pub(crate) fn for_test() -> Self {
1268 let channel = Endpoint::from_static("http://127.0.0.1:1").connect_lazy();
1269 StreamClient {
1270 inner: NodeEventStreamClient::new(channel),
1271 auth: None,
1272 allow_old_node: false,
1273 compat: Arc::default(),
1274 }
1275 }
1276}
1277
1278#[cfg(test)]
1279mod tests {
1280 use super::*;
1281 use pb::subscribe_control::Msg;
1282
1283 fn handle() -> (WatchHandle, mpsc::Receiver<pb::SubscribeControl>) {
1285 let (tx, rx) = mpsc::channel(16);
1286 (WatchHandle { tx }, rx)
1287 }
1288
1289 fn next(rx: &mut mpsc::Receiver<pb::SubscribeControl>) -> Msg {
1290 rx.try_recv().expect("a control message was sent").msg.expect("msg set")
1291 }
1292
1293 #[test]
1294 fn only_an_https_scheme_counts_as_encrypted() {
1295 assert!(endpoint_is_https("https://node.example:50051"));
1296 assert!(endpoint_is_https("HTTPS://node.example:50051"));
1297 assert!(!endpoint_is_https("http://node.example:50051"));
1298 assert!(!endpoint_is_https("node.example:50051"));
1301 assert!(!endpoint_is_https("127.0.0.1:50051"));
1302 }
1303
1304 #[test]
1310 fn encryption_requires_the_tls_feature_and_not_just_the_scheme() {
1311 assert_eq!(endpoint_is_encrypted("https://node.example:50051"), cfg!(feature = "tls"));
1312 assert!(!endpoint_is_encrypted("http://node.example:50051"));
1313 assert!(!endpoint_is_encrypted("node.example:50051"));
1314 }
1315
1316 #[test]
1319 fn the_refusal_does_not_echo_url_userinfo() {
1320 assert_eq!(
1321 redact_endpoint_userinfo("http://user:hunter2@node.example:50051"),
1322 "http://***@node.example:50051"
1323 );
1324 assert_eq!(
1325 redact_endpoint_userinfo("http://user:hunter2@node.example:50051/a@b"),
1326 "http://***@node.example:50051/a@b"
1327 );
1328 assert_eq!(redact_endpoint_userinfo("user:hunter2@node.example:50051"), "***@node.example:50051");
1329 assert_eq!(
1331 redact_endpoint_userinfo("https://node.example:50051/path"),
1332 "https://node.example:50051/path"
1333 );
1334 assert_eq!(redact_endpoint_userinfo("node.example:50051"), "node.example:50051");
1335 }
1336
1337 #[tokio::test]
1338 async fn a_password_in_the_endpoint_is_not_quoted_back() {
1339 let err = StreamClient::builder("http://user:hunter2@node.example:50051")
1340 .bearer_token("SECRET")
1341 .connect()
1342 .await
1343 .expect_err("a bearer token over plaintext must be refused");
1344 assert!(!err.to_string().contains("hunter2"), "leaked the URL password: {err}");
1345 assert!(!err.to_string().contains("SECRET"), "leaked the token: {err}");
1346 }
1347
1348 #[tokio::test]
1354 async fn a_bearer_token_over_plaintext_is_refused() {
1355 for endpoint in ["http://127.0.0.1:1", "http://node.example:50051", "node.example:50051"] {
1356 let err = StreamClient::builder(endpoint)
1357 .bearer_token("SECRET")
1358 .connect()
1359 .await
1360 .expect_err("a bearer token over plaintext must be refused");
1361 assert!(
1362 matches!(err, StreamError::InsecureCredential(_)),
1363 "{endpoint}: got {err:?}"
1364 );
1365 assert!(
1366 !err.to_string().contains("SECRET"),
1367 "{endpoint}: the error must not quote the token: {err}"
1368 );
1369 }
1370 }
1371
1372 fn assert_reached_the_transport(err: &StreamError, ctx: &str) {
1377 assert!(
1378 matches!(err, StreamError::Connect(_) | StreamError::Transport(_)),
1379 "{ctx}: expected a transport/connect error, got {err:?}"
1380 );
1381 }
1382
1383 #[tokio::test]
1387 async fn plaintext_without_a_token_is_unaffected() {
1388 let err = StreamClient::builder("http://127.0.0.1:1")
1389 .connect()
1390 .await
1391 .expect_err("nothing is listening on port 1");
1392 assert_reached_the_transport(&err, "no token");
1393 }
1394
1395 #[tokio::test]
1398 async fn the_named_waiver_permits_a_token_over_plaintext() {
1399 let err = StreamClient::builder("http://127.0.0.1:1")
1400 .insecure_bearer_token("SECRET")
1401 .connect()
1402 .await
1403 .expect_err("nothing is listening on port 1");
1404 assert_reached_the_transport(&err, "waiver");
1405 }
1406
1407 #[tokio::test]
1410 async fn the_waiver_does_not_leak_into_a_subsequent_bearer_token() {
1411 let err = StreamClient::builder("http://127.0.0.1:1")
1412 .insecure_bearer_token("SECRET")
1413 .bearer_token("SECRET")
1414 .connect()
1415 .await
1416 .expect_err("the waiver must not survive a later bearer_token");
1417 assert!(
1418 matches!(err, StreamError::InsecureCredential(_)),
1419 "got {err:?}"
1420 );
1421 }
1422
1423 #[cfg(feature = "tls")]
1427 #[tokio::test]
1428 async fn https_permits_a_token() {
1429 let err = StreamClient::builder("https://127.0.0.1:1")
1430 .tls()
1431 .bearer_token("SECRET")
1432 .connect()
1433 .await
1434 .expect_err("nothing is listening on port 1");
1435 assert!(
1436 !matches!(err, StreamError::InsecureCredential(_)),
1437 "got {err:?}"
1438 );
1439 }
1440
1441 #[test]
1442 fn subscribe_options_map_tweak_knobs_only_when_set() {
1443 let default = SubscribeOptions { categories: Categories::TWEAKS, ..Default::default() }
1446 .into_request();
1447 assert_eq!(default.mempool_tweaks, None);
1448 assert_eq!(default.tweak_outputs, None);
1449 assert_eq!(default.tweak_unspent_only, None);
1450
1451 let full = SubscribeOptions {
1452 categories: Categories::TWEAKS,
1453 mempool_tweaks: true,
1454 tweak_outputs: true,
1455 tweaks_only: true,
1456 tweak_unspent_only: true,
1457 ..Default::default()
1458 }
1459 .into_request();
1460 assert_eq!(full.mempool_tweaks, Some(true));
1461 assert_eq!(full.tweak_outputs, Some(true));
1462 assert_eq!(full.tweaks_only, Some(true));
1463 assert_eq!(full.tweak_unspent_only, Some(true));
1464 }
1465
1466 #[tokio::test]
1467 async fn add_scripts_mixed_floors_emits_parallel_min_values() {
1468 let (h, mut rx) = handle();
1469 h.add_scripts([([1u8; 32], Some(5_000)), ([2u8; 32], None)]).await.unwrap();
1470 match next(&mut rx) {
1471 Msg::AddScripts(a) => {
1472 assert_eq!(a.scripthashes.len(), 2);
1473 assert_eq!(a.min_values, vec![5_000, 0]);
1475 }
1476 other => panic!("expected AddScripts, got {other:?}"),
1477 }
1478 }
1479
1480 #[tokio::test]
1481 async fn add_scripts_no_floors_emits_empty_min_values() {
1482 let (h, mut rx) = handle();
1483 h.add_scripts([([1u8; 32], None), ([2u8; 32], None)]).await.unwrap();
1484 match next(&mut rx) {
1485 Msg::AddScripts(a) => assert!(a.min_values.is_empty()),
1486 other => panic!("expected AddScripts, got {other:?}"),
1487 }
1488 }
1489
1490 #[tokio::test]
1491 async fn empty_inputs_send_nothing() {
1492 let (h, mut rx) = handle();
1493 h.add_scripts([]).await.unwrap();
1494 h.add_outpoints([]).await.unwrap();
1495 h.add_depth_alarms([[1u8; 32]], []).await.unwrap(); h.add_depth_alarms([], [3]).await.unwrap(); h.add_depth_alarms([[1u8; 32]], [0]).await.unwrap();
1500 assert!(rx.try_recv().is_err(), "no message should have been sent");
1501 }
1502
1503 #[tokio::test]
1504 async fn lifecycle_vs_depth_alarms_dispatch_on_min_depths() {
1505 let (h, mut rx) = handle();
1506 h.add_tx_lifecycle([[7u8; 32]], AutoClose::AtDepth(6)).await.unwrap();
1507 match next(&mut rx) {
1508 Msg::AddTransactions(a) => {
1509 assert!(a.min_depths.is_empty(), "lifecycle => empty min_depths");
1510 assert_eq!(a.auto_close_depth, 6);
1511 }
1512 other => panic!("expected AddTransactions, got {other:?}"),
1513 }
1514
1515 h.add_depth_alarms([[7u8; 32], [8u8; 32]], [3, 6]).await.unwrap();
1516 match next(&mut rx) {
1517 Msg::AddTransactions(a) => {
1518 assert_eq!(a.txids.len(), 2);
1519 assert_eq!(a.min_depths, vec![3, 6]); assert_eq!(a.auto_close_depth, 0);
1521 }
1522 other => panic!("expected AddTransactions, got {other:?}"),
1523 }
1524 }
1525
1526 #[tokio::test]
1527 async fn prefix_validation_rejects_bad_length_and_bits() {
1528 let (h, mut rx) = handle();
1529 let err = h.add_script_prefixes([(vec![0xab], 16)]).await.unwrap_err();
1531 assert!(matches!(err, StreamError::InvalidArgument(_)));
1532 let err = h.add_script_prefixes([(vec![], 0)]).await.unwrap_err();
1534 assert!(matches!(err, StreamError::InvalidArgument(_)));
1535 let err = h.add_script_prefixes([(vec![0u8; 5], 33)]).await.unwrap_err();
1538 assert!(matches!(err, StreamError::InvalidArgument(_)));
1539 assert!(rx.try_recv().is_err());
1541
1542 h.add_script_prefixes([(vec![0xde, 0xad, 0xbe, 0xef], 32)]).await.unwrap();
1544 match next(&mut rx) {
1545 Msg::AddScriptPrefixes(a) => assert_eq!(a.prefixes[0].bits, 32),
1546 other => panic!("expected AddScriptPrefixes, got {other:?}"),
1547 }
1548
1549 h.add_script_prefixes([(vec![0xab, 0xcd], 16)]).await.unwrap();
1551 match next(&mut rx) {
1552 Msg::AddScriptPrefixes(a) => {
1553 assert_eq!(a.prefixes.len(), 1);
1554 assert_eq!(a.prefixes[0].bits, 16);
1555 assert_eq!(a.prefixes[0].prefix, vec![0xab, 0xcd]);
1556 }
1557 other => panic!("expected AddScriptPrefixes, got {other:?}"),
1558 }
1559 }
1560
1561 #[tokio::test]
1562 async fn set_cursor_wraps_cursor() {
1563 let (h, mut rx) = handle();
1564 let cursor = Cursor { height: 9, tx_index: 0, mempool_seq: 1, instance_id: 2 };
1565 h.set_cursor(cursor).await.unwrap();
1566 match next(&mut rx) {
1567 Msg::SetCursor(s) => assert_eq!(s.cursor, Some(cursor)),
1568 other => panic!("expected SetCursor, got {other:?}"),
1569 }
1570 }
1571
1572 #[cfg(feature = "tls")]
1575 #[test]
1576 fn tls_builder_debug_redacts_secrets() {
1577 let b = StreamClient::builder("https://node:50051")
1578 .bearer_token("SUPER_SECRET_TOKEN")
1579 .tls_ca_pem(b"-----BEGIN CERTIFICATE-----SECRET_CA_BYTES".to_vec())
1580 .tls_client_identity(b"CLIENT_CERT".to_vec(), b"CLIENT_PRIVATE_KEY".to_vec());
1581 let dbg = format!("{b:?}");
1582 assert!(dbg.contains("tls: true"), "expected tls: true, got {dbg}");
1583 assert!(dbg.contains("<redacted>"), "token should be redacted: {dbg}");
1584 assert!(!dbg.contains("SUPER_SECRET_TOKEN"));
1585 assert!(!dbg.contains("SECRET_CA_BYTES"));
1586 assert!(!dbg.contains("CLIENT_PRIVATE_KEY"));
1587 }
1588
1589 #[cfg(feature = "tls")]
1595 #[tokio::test]
1596 async fn tls_connect_assembles_and_fails_cleanly() {
1597 let err = StreamClient::builder("https://127.0.0.1:1")
1598 .tls()
1599 .tls_domain("node.example")
1600 .connect()
1601 .await
1602 .expect_err("connect to a dead port must fail");
1603 assert!(
1604 !matches!(err, StreamError::InvalidEndpoint(_) | StreamError::InvalidToken),
1605 "expected a transport/connect error, got {err:?}",
1606 );
1607 }
1608
1609 #[cfg(feature = "tls")]
1615 #[tokio::test]
1616 async fn tls_over_non_https_endpoint_is_refused() {
1617 for ep in ["http://127.0.0.1:1", "127.0.0.1:1"] {
1618 let err = StreamClient::builder(ep)
1619 .tls_ca_pem(b"-----BEGIN CERTIFICATE-----".to_vec())
1620 .bearer_token("SECRET")
1621 .connect()
1622 .await
1623 .expect_err("TLS over non-https must be refused");
1624 match err {
1625 StreamError::InvalidEndpoint(msg) => assert!(
1626 msg.contains("https"),
1627 "expected an https-scheme error for {ep}, got {msg}"
1628 ),
1629 other => panic!("expected InvalidEndpoint for {ep}, got {other:?}"),
1630 }
1631 }
1632 }
1633}
1634
1635#[cfg(test)]
1638mod version_check_tests {
1639 use super::*;
1640 use std::sync::atomic::{AtomicUsize, Ordering};
1641
1642 use satd_events_proto::v1::node_event_stream_server::{
1643 NodeEventStream, NodeEventStreamServer,
1644 };
1645 use tokio_stream::Stream;
1646
1647 use crate::compat::{Version, SDK_VERSION};
1648 use crate::{ResilientConfig, ResilientWatchConfig};
1649
1650 type EventResult = Result<pb::NodeEvent, tonic::Status>;
1651 type BoxStream = std::pin::Pin<Box<dyn Stream<Item = EventResult> + Send>>;
1652
1653 #[derive(Clone)]
1656 struct FakeNode {
1657 version: Arc<Mutex<Option<String>>>,
1658 schema: Option<&'static str>,
1659 event_schema: Option<u32>,
1661 opens: Arc<AtomicUsize>,
1662 }
1663
1664 impl FakeNode {
1665 fn new(version: Option<&str>) -> Self {
1666 FakeNode {
1667 version: Arc::new(Mutex::new(version.map(str::to_owned))),
1668 schema: Some("1"),
1669 event_schema: None,
1670 opens: Arc::default(),
1671 }
1672 }
1673
1674 fn respond(&self) -> tonic::Response<BoxStream> {
1675 self.opens.fetch_add(1, Ordering::SeqCst);
1676 let events: Vec<EventResult> = self
1677 .event_schema
1678 .map(|schema_version| {
1679 Ok(pb::NodeEvent {
1680 schema_version,
1681 body: Some(pb::node_event::Body::Heartbeat(pb::Heartbeat {
1682 uptime_ns: 1,
1683 })),
1684 ..Default::default()
1685 })
1686 })
1687 .into_iter()
1688 .collect();
1689 let stream = tokio_stream::StreamExt::chain(
1690 tokio_stream::iter(events),
1691 tokio_stream::pending(),
1692 );
1693 let mut resp = tonic::Response::new(Box::pin(stream) as BoxStream);
1694 let md = resp.metadata_mut();
1695 if let Some(v) = self.version.lock().unwrap().as_deref() {
1696 md.insert(compat::VERSION_HEADER, v.parse().unwrap());
1697 }
1698 if let Some(s) = self.schema {
1699 md.insert(compat::SCHEMA_HEADER, s.parse().unwrap());
1700 }
1701 resp
1702 }
1703 }
1704
1705 #[tonic::async_trait]
1706 impl NodeEventStream for FakeNode {
1707 type SubscribeStream = BoxStream;
1708 type WatchStream = BoxStream;
1709
1710 async fn subscribe(
1711 &self,
1712 _: tonic::Request<pb::SubscribeRequest>,
1713 ) -> Result<tonic::Response<BoxStream>, tonic::Status> {
1714 Ok(self.respond())
1715 }
1716
1717 async fn watch(
1718 &self,
1719 _: tonic::Request<tonic::Streaming<pb::SubscribeControl>>,
1720 ) -> Result<tonic::Response<BoxStream>, tonic::Status> {
1721 Ok(self.respond())
1722 }
1723 }
1724
1725 async fn serve(fake: FakeNode) -> String {
1726 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1727 let addr = listener.local_addr().unwrap();
1728 let incoming =
1729 tonic::transport::server::TcpIncoming::from_listener(listener, true, None).unwrap();
1730 tokio::spawn(
1731 tonic::transport::Server::builder()
1732 .add_service(NodeEventStreamServer::new(fake))
1733 .serve_with_incoming(incoming),
1734 );
1735 format!("http://{addr}")
1736 }
1737
1738 async fn client_for(fake: &FakeNode) -> StreamClient {
1739 StreamClient::builder(serve(fake.clone()).await).connect().await.expect("connect")
1740 }
1741
1742 fn sdk() -> Version {
1743 compat::parse(SDK_VERSION).unwrap()
1744 }
1745
1746 fn minors_behind(n: u32) -> Option<String> {
1750 let sdk = sdk();
1751 (sdk.minor >= n).then(|| format!("{}.{}.0", sdk.major, sdk.minor - n))
1752 }
1753
1754 fn too_old() -> String {
1757 minors_behind(2).unwrap_or_else(|| format!("{}.9.0", sdk().major - 1))
1758 }
1759
1760 thread_local! {
1761 static THREAD_LOGS: std::cell::RefCell<Vec<u8>> =
1763 const { std::cell::RefCell::new(Vec::new()) };
1764 }
1765
1766 struct ThreadLogs;
1767
1768 thread_local! {
1769 static PROBE_COMPAT: std::cell::RefCell<Option<Arc<Mutex<CompatState>>>> =
1773 const { std::cell::RefCell::new(None) };
1774 static PROBE_FREE: std::cell::RefCell<Vec<bool>> =
1777 const { std::cell::RefCell::new(Vec::new()) };
1778 }
1779
1780 struct ProbeLock;
1784
1785 impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for ProbeLock {
1786 fn on_event(
1787 &self,
1788 _: &tracing::Event<'_>,
1789 _: tracing_subscriber::layer::Context<'_, S>,
1790 ) {
1791 PROBE_COMPAT.with(|p| {
1792 if let Some(compat) = p.borrow().as_ref() {
1793 let free = compat.try_lock().is_ok();
1794 PROBE_FREE.with(|f| f.borrow_mut().push(free));
1795 }
1796 });
1797 }
1798 }
1799
1800 struct Probe;
1802
1803 impl Probe {
1804 fn arm(compat: Arc<Mutex<CompatState>>) -> Self {
1805 PROBE_FREE.with(|f| f.borrow_mut().clear());
1806 PROBE_COMPAT.with(|p| *p.borrow_mut() = Some(compat));
1807 Probe
1808 }
1809
1810 fn seen(&self) -> Vec<bool> {
1811 PROBE_FREE.with(|f| f.borrow().clone())
1812 }
1813 }
1814
1815 impl Drop for Probe {
1816 fn drop(&mut self) {
1817 PROBE_COMPAT.with(|p| *p.borrow_mut() = None);
1818 }
1819 }
1820
1821 impl std::io::Write for ThreadLogs {
1822 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1823 THREAD_LOGS.with(|l| l.borrow_mut().extend_from_slice(buf));
1824 Ok(buf.len())
1825 }
1826 fn flush(&mut self) -> std::io::Result<()> {
1827 Ok(())
1828 }
1829 }
1830
1831 struct Logs;
1843
1844 impl Logs {
1845 fn capture() -> Self {
1846 static INIT: std::sync::Once = std::sync::Once::new();
1847 INIT.call_once(|| {
1848 use tracing_subscriber::layer::SubscriberExt as _;
1849 use tracing_subscriber::util::SubscriberInitExt as _;
1850 let _ = tracing_subscriber::registry()
1851 .with(
1852 tracing_subscriber::fmt::layer()
1853 .with_ansi(false)
1854 .with_writer(|| ThreadLogs),
1855 )
1856 .with(ProbeLock)
1857 .with(tracing_subscriber::filter::LevelFilter::TRACE)
1858 .try_init();
1859 });
1860 THREAD_LOGS.with(|l| l.borrow_mut().clear());
1861 Logs
1862 }
1863
1864 fn lines(&self) -> Vec<String> {
1865 THREAD_LOGS.with(|l| {
1866 String::from_utf8(l.borrow().clone())
1867 .unwrap()
1868 .lines()
1869 .map(str::to_owned)
1870 .collect()
1871 })
1872 }
1873
1874 fn warnings(&self) -> Vec<String> {
1875 self.lines().into_iter().filter(|l| l.contains("WARN")).collect()
1876 }
1877 }
1878
1879 #[tokio::test]
1880 async fn same_version_node_opens_silently() {
1881 let logs = Logs::capture();
1882 let fake = FakeNode::new(Some(SDK_VERSION));
1883 let mut client = client_for(&fake).await;
1884 client.subscribe(SubscribeOptions::default()).await.expect("subscribe");
1885 client.watch().await.expect("watch");
1886 assert!(logs.warnings().is_empty(), "{:?}", logs.warnings());
1887 }
1888
1889 #[tokio::test]
1890 async fn newer_node_opens_silently() {
1891 let logs = Logs::capture();
1892 let sdk = sdk();
1893 let fake = FakeNode::new(Some(&format!("{}.{}.0", sdk.major, sdk.minor + 3)));
1894 let mut client = client_for(&fake).await;
1895 client.subscribe(SubscribeOptions::default()).await.expect("subscribe");
1896 assert!(logs.warnings().is_empty(), "{:?}", logs.warnings());
1897 }
1898
1899 #[tokio::test]
1902 async fn headerless_node_is_treated_as_0_5() {
1903 let mut fake = FakeNode::new(None);
1904 fake.schema = None;
1905 let mut client = client_for(&fake).await;
1906 let res = client.subscribe(SubscribeOptions::default()).await;
1907 match compat::classify(sdk(), compat::HEADERLESS_NODE) {
1908 Compat::TooOld => assert!(matches!(res, Err(StreamError::NodeTooOld { .. }))),
1909 _ => assert!(res.is_ok(), "{:?}", res.err()),
1910 }
1911 assert_eq!(client.node_version(), None);
1912 }
1913
1914 #[tokio::test]
1915 async fn two_minors_behind_is_refused() {
1916 let fake = FakeNode::new(Some(&too_old()));
1917 let mut client = client_for(&fake).await;
1918 let err = client.subscribe(SubscribeOptions::default()).await.err().expect("refused");
1919 match &err {
1920 StreamError::NodeTooOld { node, sdk } => {
1921 assert_eq!(node, &too_old());
1922 assert_eq!(*sdk, SDK_VERSION);
1923 }
1924 other => panic!("expected NodeTooOld, got {other:?}"),
1925 }
1926 assert!(!err.is_retryable());
1927 let err = client.watch().await.err().expect("watch refused too");
1928 assert!(matches!(err, StreamError::NodeTooOld { .. }), "{err:?}");
1929 }
1930
1931 #[tokio::test]
1932 async fn allow_old_node_downgrades_the_refusal_to_a_warning() {
1933 let logs = Logs::capture();
1934 let fake = FakeNode::new(Some(&too_old()));
1935 let mut client = StreamClient::builder(serve(fake.clone()).await)
1936 .allow_old_node()
1937 .connect()
1938 .await
1939 .unwrap();
1940 client.subscribe(SubscribeOptions::default()).await.expect("allowed");
1941 assert_eq!(logs.warnings().len(), 1, "{:?}", logs.warnings());
1942 }
1943
1944 #[tokio::test]
1945 async fn schema_header_mismatch_is_refused_even_with_allow_old_node() {
1946 let mut fake = FakeNode::new(Some(SDK_VERSION));
1947 fake.schema = Some("2");
1948 let mut client = StreamClient::builder(serve(fake.clone()).await)
1949 .allow_old_node()
1950 .connect()
1951 .await
1952 .unwrap();
1953 let err = client.subscribe(SubscribeOptions::default()).await.err().expect("refused");
1954 assert!(matches!(err, StreamError::SchemaMismatch { node: 2, sdk: 1 }), "{err:?}");
1955 assert!(!err.is_retryable());
1956 let err = client.watch().await.err().expect("watch refused too");
1957 assert!(matches!(err, StreamError::SchemaMismatch { node: 2, sdk: 1 }), "{err:?}");
1958 }
1959
1960 #[tokio::test]
1963 async fn first_event_schema_mismatch_is_refused() {
1964 let mut fake = FakeNode::new(Some(SDK_VERSION));
1965 fake.event_schema = Some(2);
1966 let mut client = client_for(&fake).await;
1967 let mut stream = client.subscribe(SubscribeOptions::default()).await.expect("opens");
1968 let err = stream.message().await.expect_err("refused");
1969 assert!(matches!(err, StreamError::SchemaMismatch { node: 2, sdk: 1 }), "{err:?}");
1970
1971 let mut fake = FakeNode::new(Some(SDK_VERSION));
1973 fake.event_schema = Some(1);
1974 let mut client = client_for(&fake).await;
1975 let (_handle, mut stream) = client.watch().await.expect("opens");
1976 let ev = stream.message().await.expect("event").expect("not closed");
1977 assert!(matches!(ev, Event::Heartbeat { .. }), "{ev:?}");
1978 }
1979
1980 #[tokio::test]
1981 async fn one_behind_warns_once_per_node_version() {
1982 let logs = Logs::capture();
1983 let (behind, allow) = match minors_behind(1) {
1986 Some(v) => (v, false),
1987 None => (too_old(), true),
1988 };
1989 let fake = FakeNode::new(Some(&behind));
1990 let mut builder = StreamClient::builder(serve(fake.clone()).await);
1991 if allow {
1992 builder = builder.allow_old_node();
1993 }
1994 let mut client = builder.connect().await.unwrap();
1995
1996 client.subscribe(SubscribeOptions::default()).await.expect("subscribe");
1997 client.subscribe(SubscribeOptions::default()).await.expect("subscribe again");
1998 client.clone().watch().await.expect("watch");
2000 let warnings = logs.warnings();
2001 assert_eq!(warnings.len(), 1, "{warnings:?}");
2002 assert!(warnings[0].contains(&format!("node_version={behind}")), "{warnings:?}");
2003 assert!(warnings[0].contains(&format!("sdk_version=\"{SDK_VERSION}\"")), "{warnings:?}");
2004 assert!(warnings[0].contains("is older than this SDK"), "{warnings:?}");
2005
2006 *fake.version.lock().unwrap() = Some(format!("{behind}-other"));
2008 client.subscribe(SubscribeOptions::default()).await.expect("subscribe");
2009 client.subscribe(SubscribeOptions::default()).await.expect("subscribe");
2010 assert_eq!(logs.warnings().len(), 2, "{:?}", logs.warnings());
2011 }
2012
2013 #[tokio::test]
2017 async fn warning_is_logged_without_holding_the_compat_lock() {
2018 let _logs = Logs::capture();
2019 let (behind, allow) = match minors_behind(1) {
2020 Some(v) => (v, false),
2021 None => (too_old(), true),
2022 };
2023 let fake = FakeNode::new(Some(&behind));
2024 let mut builder = StreamClient::builder(serve(fake.clone()).await);
2025 if allow {
2026 builder = builder.allow_old_node();
2027 }
2028 let mut client = builder.connect().await.unwrap();
2029
2030 let probe = Probe::arm(client.compat.clone());
2031 client.subscribe(SubscribeOptions::default()).await.expect("subscribe");
2032 let seen = probe.seen();
2033 assert!(!seen.is_empty(), "the warning was not logged");
2034 assert!(seen.iter().all(|f| *f), "an event was logged under the compat lock: {seen:?}");
2035 }
2036
2037 #[tokio::test]
2038 async fn node_version_is_recorded_once_a_stream_opens() {
2039 let fake = FakeNode::new(Some(SDK_VERSION));
2040 let client = client_for(&fake).await;
2041 assert_eq!(client.node_version(), None);
2042 client.clone().subscribe(SubscribeOptions::default()).await.expect("subscribe");
2043 assert_eq!(client.node_version().as_deref(), Some(SDK_VERSION));
2044 }
2045
2046 #[tokio::test]
2047 async fn resilient_subscribe_surfaces_node_too_old_without_retrying() {
2048 let fake = FakeNode::new(Some(&too_old()));
2049 let client = client_for(&fake).await;
2050 let mut sub =
2051 client.resilient_subscribe(SubscribeOptions::default(), ResilientConfig::default());
2052 let err = tokio::time::timeout(Duration::from_secs(5), sub.next())
2053 .await
2054 .expect("no backoff loop")
2055 .expect_err("refused");
2056 assert!(matches!(err, StreamError::NodeTooOld { .. }), "{err:?}");
2057 assert_eq!(fake.opens.load(Ordering::SeqCst), 1);
2058 }
2059
2060 #[tokio::test]
2061 async fn resilient_watch_surfaces_node_too_old_without_retrying() {
2062 let fake = FakeNode::new(Some(&too_old()));
2063 let client = client_for(&fake).await;
2064 let mut watch = client.resilient_watch(ResilientWatchConfig::default());
2065 let err = tokio::time::timeout(Duration::from_secs(5), watch.next())
2066 .await
2067 .expect("no backoff loop")
2068 .expect_err("refused");
2069 assert!(matches!(err, StreamError::NodeTooOld { .. }), "{err:?}");
2070 assert_eq!(fake.opens.load(Ordering::SeqCst), 1);
2071 }
2072}