Skip to main content

thornode_pulse/
lib.rs

1//! Rust client SDK for the **pulse** QUIC decoded-shred transaction stream
2//! (wire v2).
3//!
4//! Two tiers, one connection each (the server selects the tier from the first
5//! control message, which this SDK always negotiates to wire v2 —
6//! [`thornode_pulse_wire::frame::WIRE_VERSION`]):
7//!   * [`PulseClient::subscribe_sig_first`] — the low-latency **sig-first**
8//!     tier. One QUIC DATAGRAM per tx ([`SigFirstItem`]: slot, per-subscriber
9//!     `seq`, signature), fire-and-forget, no head-of-line blocking.
10//!     [`SigFirstSub::gaps`] counts sequence numbers this subscriber may not
11//!     have received (see its docs for the exact, honest guarantee — QUIC
12//!     datagrams are unordered, so it over-reports under reordering).
13//!   * [`PulseClient::subscribe_full`] — the **full-tx** tier. A single
14//!     ordered QUIC stream that opens with a 6-byte preamble (this SDK
15//!     reads and verifies it before the subscription is ever handed back —
16//!     see [`Error::BadPreamble`]), then length-delimited, fully-decoded
17//!     transaction frames ([`Frame::Tx`] wrapping [`FullTxV2`]). Stream bytes
18//!     are ordered/reliable after the server enqueues them; the server's
19//!     bounded pre-stream queue may shed transactions before that point.
20//!
21//! Both tiers also carry periodic heartbeats (idle-stream liveness, plus
22//! `highest_seq` — the highest sequence number assigned to this subscriber so
23//! far; `u64::MAX` means none yet, see [`NO_SEQ_ASSIGNED`]). A heartbeat is
24//! folded into [`FullSub::heartbeat`] / [`SigFirstSub::gaps`] rather than
25//! handed back as an item, and a message or datagram type this SDK doesn't
26//! recognize is skipped rather than treated as an error — that is what keeps
27//! a future wire addition from breaking this client (see [`Frame::Unknown`] /
28//! [`Datagram::Unknown`]).
29//!
30//! ```no_run
31//! use thornode_pulse::{Filter, PulseClient};
32//! # async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
33//! let endpoint = std::env::var("PULSE_ADDR")
34//!     .expect("set PULSE_ADDR to <HOST:PORT_FROM_DASHBOARD>");
35//! let token = std::env::var("PULSE_TOKEN")
36//!     .expect("set PULSE_TOKEN to <TOKEN_FROM_SAME_LOCATION>");
37//! let account = std::env::var("PULSE_ACCOUNT")
38//!     .expect("set PULSE_ACCOUNT to <ACCOUNT_OR_PROGRAM_PUBKEY>");
39//! let client = PulseClient::connect_with_token(endpoint, token).await?;
40//! let mut sub = client.subscribe_sig_first(&Filter::accounts([account])).await?;
41//! while let Some(item) = sub.next().await? {
42//!     println!("slot {} seq {} sig {}", item.slot, item.seq, bs58::encode(item.signature).into_string());
43//! }
44//! # Ok(()) }
45//! ```
46//!
47//! The wire protocol is documented in `docs/PROTOCOL.md`. Frame/datagram
48//! decoders and derived-field helpers are provided by `thornode-pulse-wire`
49//! and re-exported here.
50
51use std::net::{IpAddr, SocketAddr};
52use std::sync::atomic::{AtomicU64, Ordering};
53use std::sync::{Arc, OnceLock};
54use std::time::Duration;
55
56use serde::{Deserialize, Serialize};
57use thornode_pulse_wire::frame::{decode_datagram, decode_frame};
58use tokio::io::AsyncReadExt;
59
60pub use thornode_pulse_wire::derive::{
61    compute_unit_limit, compute_unit_price, fee_payer, program_ids, static_writable_accounts,
62};
63pub use thornode_pulse_wire::frame::{Datagram, Frame, FullTx, FullTxV2};
64pub use thornode_pulse_wire::protocol::RetryClass;
65
66/// Wire sentinel for a heartbeat's `highest_seq` meaning "nothing has been
67/// assigned to this subscriber yet". `0` is a real, already-assigned sequence
68/// number (the FIRST delivery on any connection is `seq == 0`), so `0` cannot
69/// double as "none" — conflating the two would tell a client it already
70/// missed transaction 0 the instant it connected.
71pub const NO_SEQ_ASSIGNED: u64 = u64::MAX;
72
73/// Subscription filter — the account predicate model the server applies. An
74/// empty account filter ([`Filter::all`]) selects the unfiltered non-vote feed;
75/// the access selected for the connection determines whether that feed is
76/// available. Vote transactions remain excluded unless [`Filter::with_vote`]
77/// is `true`.
78#[derive(Debug, Clone, Default, Serialize)]
79pub struct Filter {
80    #[serde(skip_serializing_if = "Vec::is_empty")]
81    pub account_include: Vec<String>,
82    #[serde(skip_serializing_if = "Vec::is_empty")]
83    pub account_exclude: Vec<String>,
84    #[serde(skip_serializing_if = "Vec::is_empty")]
85    pub account_required: Vec<String>,
86    /// Vote selection (Yellowstone parity): `Some(true)` selects vote-only;
87    /// `Some(false)` selects non-vote-only. Omitted by default (`None`), in
88    /// which case the server also selects non-votes. One subscription cannot
89    /// combine both sets.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub vote: Option<bool>,
92}
93
94impl Filter {
95    /// Subscribe without an account predicate. Votes remain excluded by the
96    /// server default; use [`Filter::with_vote`] with a separate connection to
97    /// select the vote-only feed.
98    pub fn all() -> Self {
99        Filter::default()
100    }
101
102    /// Subscribe to transactions touching any of `accounts` (base58 pubkeys /
103    /// program ids).
104    pub fn accounts<I, S>(accounts: I) -> Self
105    where
106        I: IntoIterator<Item = S>,
107        S: Into<String>,
108    {
109        Filter {
110            account_include: accounts.into_iter().map(Into::into).collect(),
111            ..Default::default()
112        }
113    }
114
115    /// Selects vote-only (`true`) or non-vote-only (`false`). Without this, the
116    /// field is omitted and the server default selects non-votes. Use two
117    /// connections when an application needs both sets.
118    pub fn with_vote(mut self, include: bool) -> Self {
119        self.vote = Some(include);
120        self
121    }
122}
123
124/// The JSON control message sent on a bi-directional stream. `v` always
125/// declares wire v2 (this SDK speaks no other version); `full` selects the
126/// tier and is only honored on the connection's FIRST control message;
127/// `fields` opts into per-frame enrichment groups (currently just `"alt"`)
128/// and is only meaningful on the full-tx tier — the sig-first tier carries no
129/// enrichment under any subscription, so the server simply ignores it there.
130#[derive(Serialize)]
131struct Control<'a> {
132    #[serde(flatten)]
133    filter: &'a Filter,
134    #[serde(skip_serializing_if = "str::is_empty")]
135    token: &'a str,
136    full: bool,
137    v: u32,
138    fields: &'a [&'a str],
139}
140
141/// A parsed `{"type":"...","ok":bool,...}` control-channel envelope — the
142/// server's answer to any control message (first or update).
143#[derive(Debug, Clone, Deserialize)]
144pub struct Ack {
145    /// Envelope discriminator. Only `"ack"` and `"error"` are valid; it is
146    /// optional at deserialization time so validation can turn a missing or
147    /// unknown value into a stable [`Error::BadFrame`] instead of exposing a
148    /// serde implementation detail.
149    #[serde(rename = "type", default)]
150    pub message_type: Option<String>,
151    /// The server's `error` envelope carries no `ok` field. Defaulting to
152    /// `false` preserves it as a rejection so its code and reason remain
153    /// available to the caller.
154    #[serde(default)]
155    pub ok: bool,
156    /// Present when `ok` is `false`: why the message was rejected.
157    #[serde(default)]
158    pub reason: Option<String>,
159    /// Present on a terminal error envelope. When supplied, the SDK preserves
160    /// the same typed close/retry semantics as a QUIC application close.
161    #[serde(default)]
162    pub code: Option<u64>,
163    /// Present only on the FIRST control message's ack: the wire version the
164    /// server actually negotiated (`min(client_max, SERVER_WIRE_VERSION)`).
165    #[serde(default)]
166    pub v: Option<u32>,
167}
168
169/// Errors surfaced by the client.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum Error {
172    InvalidEndpoint(String),
173    Connect(String),
174    ConnectTimeout,
175    Io(String),
176    Tls(String),
177    /// An explicit insecure-local-dev constructor was given a non-loopback
178    /// address. Unverified TLS is never permitted for a public endpoint.
179    InsecureEndpointNotLoopback(SocketAddr),
180    /// The peer terminated the connection with a Pulse application close.
181    ApplicationClosed(CloseInfo),
182    /// A datagram or stream frame that did not match the documented layout.
183    BadFrame,
184    /// A full-tx frame was truncated and the peer also supplied a terminal
185    /// application close. Both signals matter: the close explains why the
186    /// transport ended, while the framing error proves the final announced
187    /// frame was incomplete.
188    BadFrameWithClose(CloseInfo),
189    /// The full-tx stream's opening bytes were not
190    /// `thornode_pulse_wire::frame::PREAMBLE`. The peer is not serving wire
191    /// v2, or the stream was corrupted in transit. This remains distinct from
192    /// `BadFrame` so callers can identify a protocol mismatch at setup.
193    BadPreamble,
194    /// The full-tx preamble was incomplete or invalid and the peer also sent
195    /// an application close. Both the protocol error and close context are
196    /// preserved.
197    BadPreambleWithClose(CloseInfo),
198    /// The server answered a control message with `{"ok": false, ...}`.
199    /// Carries the server's stated reason.
200    Rejected(String),
201    /// No complete control ack arrived within [`ACK_TIMEOUT`]. The subscribe
202    /// call returns this error instead of waiting indefinitely.
203    AckTimeout,
204    /// The server acknowledged a full-tx subscription but did not open and
205    /// preface its stream within [`FULL_STREAM_TIMEOUT`].
206    FullStreamTimeout,
207    /// The server's first-control-message ack named a negotiated wire version
208    /// this SDK does not speak. Carries the version the server chose.
209    VersionMismatch(u32),
210    /// A successful first ack omitted `v`, leaving the datagram-only tier with
211    /// no proof that wire v2 was negotiated.
212    MissingVersion,
213}
214
215impl std::fmt::Display for Error {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        match self {
218            Error::InvalidEndpoint(e) => write!(f, "invalid endpoint: {e}"),
219            Error::Connect(e) => write!(f, "connect: {e}"),
220            Error::ConnectTimeout => write!(
221                f,
222                "timed out after {}s connecting to the server",
223                CONNECT_TIMEOUT.as_secs()
224            ),
225            Error::Io(e) => write!(f, "io: {e}"),
226            Error::Tls(e) => write!(f, "tls: {e}"),
227            Error::InsecureEndpointNotLoopback(addr) => write!(
228                f,
229                "insecure local-dev TLS is restricted to loopback addresses, got {addr}"
230            ),
231            Error::ApplicationClosed(close) => write!(
232                f,
233                "server closed the connection (code {}): {}",
234                close.code, close.reason
235            ),
236            Error::BadFrame => write!(f, "malformed frame"),
237            Error::BadFrameWithClose(close) => write!(
238                f,
239                "truncated frame before server close (code {}): {}",
240                close.code, close.reason
241            ),
242            Error::BadPreamble => write!(
243                f,
244                "bad stream preamble: this server is not speaking pulse wire v2"
245            ),
246            Error::BadPreambleWithClose(close) => write!(
247                f,
248                "bad stream preamble before server close (code {}): {}",
249                close.code, close.reason
250            ),
251            Error::Rejected(reason) => write!(f, "control message rejected: {reason}"),
252            Error::AckTimeout => write!(
253                f,
254                "timed out after {}s waiting for the server's control ack",
255                ACK_TIMEOUT.as_secs()
256            ),
257            Error::FullStreamTimeout => write!(
258                f,
259                "timed out after {}s waiting for the full-tx stream and preamble",
260                FULL_STREAM_TIMEOUT.as_secs()
261            ),
262            Error::VersionMismatch(v) => write!(
263                f,
264                "server negotiated wire v{v}, this SDK speaks only wire v{}",
265                thornode_pulse_wire::frame::WIRE_VERSION
266            ),
267            Error::MissingVersion => write!(
268                f,
269                "successful initial control ack omitted the negotiated wire version"
270            ),
271        }
272    }
273}
274impl std::error::Error for Error {}
275
276pub type Result<T> = std::result::Result<T, Error>;
277
278/// A terminal QUIC application close sent by the Pulse server.
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct CloseInfo {
281    pub code: u64,
282    pub reason: String,
283}
284
285impl CloseInfo {
286    /// Stable retry classification for Pulse close codes 0–5. Unknown codes
287    /// stay unknown rather than being guessed retryable.
288    pub fn retry_class(&self) -> RetryClass {
289        thornode_pulse_wire::protocol::classify_close_code(self.code)
290    }
291
292    /// `true` only for code 3 (transient admission/capacity). Code 2 requires
293    /// new credentials first; codes 1, 4 and 5 must not be retried unchanged.
294    pub fn retryable(&self) -> bool {
295        self.retry_class() == RetryClass::Transient
296    }
297}
298
299impl Error {
300    /// Returns the terminal application close carried by this error, including
301    /// a close that coincided with a truncated full-tx frame.
302    pub fn close_info(&self) -> Option<&CloseInfo> {
303        match self {
304            Error::ApplicationClosed(close)
305            | Error::BadFrameWithClose(close)
306            | Error::BadPreambleWithClose(close) => Some(close),
307            _ => None,
308        }
309    }
310
311    /// Whether this error reports malformed/truncated wire framing.
312    pub fn is_bad_frame(&self) -> bool {
313        matches!(self, Error::BadFrame | Error::BadFrameWithClose(_))
314    }
315
316    /// Whether this error reports an invalid or truncated full-tx preamble.
317    pub fn is_bad_preamble(&self) -> bool {
318        matches!(self, Error::BadPreamble | Error::BadPreambleWithClose(_))
319    }
320}
321
322/// UDP receive buffer requested for the client socket, in bytes.
323///
324/// The kernel may clamp this to the platform's configured receive-buffer limit.
325pub const DEFAULT_RECV_BUFFER: usize = 8 << 20; // 8 MiB
326
327/// Bound for DNS resolution plus the QUIC/TLS handshake.
328pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
329
330/// Bound for the server to open and preface the full-tx stream after acking.
331pub const FULL_STREAM_TIMEOUT: Duration = Duration::from_secs(10);
332
333/// Binds the client's UDP socket with an enlarged receive buffer.
334///
335/// The buffer request is best-effort. The kernel may clamp it to `rmem_max`,
336/// so a smaller resulting buffer is not a connection error.
337fn client_socket(addr: SocketAddr, recv_buffer: usize) -> std::io::Result<std::net::UdpSocket> {
338    let sock = socket2::Socket::new(
339        socket2::Domain::for_address(addr),
340        socket2::Type::DGRAM,
341        Some(socket2::Protocol::UDP),
342    )?;
343    // Ignore the error: some platforms reject an oversized request outright, and
344    // an undersized buffer is a performance problem, not a connection failure.
345    let _ = sock.set_recv_buffer_size(recv_buffer);
346    sock.bind(&addr.into())?;
347    sock.set_nonblocking(true)?;
348    Ok(sock.into())
349}
350
351/// A connected pulse client. Pick exactly one tier per connection.
352pub struct PulseClient {
353    conn: quinn::Connection,
354    _endpoint: quinn::Endpoint,
355    token: Option<String>,
356}
357
358impl PulseClient {
359    /// Creates a verified-TLS client builder for `host:port`.
360    pub fn builder(endpoint: impl Into<String>) -> PulseClientBuilder {
361        PulseClientBuilder::new(endpoint)
362    }
363
364    /// Resolves `host:port`, uses the host as TLS SNI, verifies the certificate
365    /// against native system roots, and negotiates QUIC ALPN `pulse`.
366    pub async fn connect(endpoint: impl Into<String>) -> Result<Self> {
367        Self::builder(endpoint).connect().await
368    }
369
370    /// Verified-TLS connection that sends `token` only after the certificate
371    /// and hostname handshake succeeds.
372    pub async fn connect_with_token(
373        endpoint: impl Into<String>,
374        token: impl Into<String>,
375    ) -> Result<Self> {
376        Self::builder(endpoint).with_token(token).connect().await
377    }
378
379    /// Connects without certificate verification for an in-process/local test
380    /// server. The address must be loopback; a public address is rejected
381    /// before any packet or bearer token is sent.
382    pub async fn dangerous_connect_insecure_local_dev(addr: SocketAddr) -> Result<Self> {
383        connect_to(
384            addr,
385            "localhost".to_owned(),
386            None,
387            Trust::InsecureLocalDev,
388            CONNECT_TIMEOUT,
389        )
390        .await
391    }
392
393    /// Token-bearing counterpart to
394    /// [`PulseClient::dangerous_connect_insecure_local_dev`]. This remains
395    /// loopback-only and is intended solely for local auth integration tests.
396    pub async fn dangerous_connect_insecure_local_dev_with_token(
397        addr: SocketAddr,
398        token: impl Into<String>,
399    ) -> Result<Self> {
400        connect_to(
401            addr,
402            "localhost".to_owned(),
403            Some(token.into()),
404            Trust::InsecureLocalDev,
405            CONNECT_TIMEOUT,
406        )
407        .await
408    }
409
410    /// Subscribes to the **sig-first** DATAGRAM tier. Yields [`SigFirstItem`]
411    /// per transaction, lowest latency. Enrichment fields are a full-tx-only
412    /// concept and are deliberately absent from this API.
413    pub async fn subscribe_sig_first(self, filter: &Filter) -> Result<SigFirstSub> {
414        let ack = self.send_control(filter, false, &[]).await?;
415        ensure_initial_ack(&ack)?;
416        Ok(SigFirstSub::spawn(self.conn))
417    }
418
419    /// Subscribes to the **full-tx** tier. Yields decoded [`Frame::Tx`] values
420    /// in stream order (heartbeats and unknown frame types are
421    /// filtered out by [`FullSub::next`] itself — see its doc comment).
422    /// `fields` requests enrichment groups (currently just `["alt"]`, which
423    /// adds each frame's ALT-loaded addresses).
424    ///
425    /// The stream's 6-byte preamble is read and verified before the
426    /// subscription is returned. A mismatch returns [`Error::BadPreamble`].
427    pub async fn subscribe_full(self, filter: &Filter, fields: &[&str]) -> Result<FullSub> {
428        let ack = self.send_control(filter, true, fields).await?;
429        ensure_initial_ack(&ack)?;
430        // The server opens exactly one unidirectional stream for this tier.
431        // Bound both its arrival and its preamble; either could otherwise wait
432        // forever after a peer sends a successful control ack and goes quiet.
433        let setup = async {
434            let mut recv = self
435                .conn
436                .accept_uni()
437                .await
438                .map_err(|e| Error::Io(e.to_string()))?;
439            verify_preamble(&mut recv).await?;
440            Ok(recv)
441        };
442        let recv = match tokio::time::timeout(FULL_STREAM_TIMEOUT, setup).await {
443            Ok(result) => {
444                result.map_err(|e| merge_wire_and_terminal(e, terminal_error(&self.conn)))?
445            }
446            Err(_) => return Err(terminal_error(&self.conn).unwrap_or(Error::FullStreamTimeout)),
447        };
448        Ok(FullSub {
449            conn: self.conn,
450            recv,
451            buf: Vec::with_capacity(4096),
452            last_heartbeat: None,
453        })
454    }
455
456    async fn send_control(&self, filter: &Filter, full: bool, fields: &[&str]) -> Result<Ack> {
457        let token = self.token.as_deref().unwrap_or("");
458        control_round_trip(&self.conn, filter, full, fields, token).await
459    }
460}
461
462/// Builder for verified production connections. Native system roots are
463/// always loaded; custom CA certificates are additive, which supports private
464/// PKI without weakening verification for public endpoints.
465pub struct PulseClientBuilder {
466    endpoint: String,
467    token: Option<String>,
468    custom_ca_der: Vec<Vec<u8>>,
469}
470
471impl PulseClientBuilder {
472    pub fn new(endpoint: impl Into<String>) -> Self {
473        Self {
474            endpoint: endpoint.into(),
475            token: None,
476            custom_ca_der: Vec::new(),
477        }
478    }
479
480    pub fn with_token(mut self, token: impl Into<String>) -> Self {
481        self.token = Some(token.into());
482        self
483    }
484
485    /// Adds a DER-encoded trust anchor while retaining native system roots.
486    /// The certificate is still checked for the endpoint hostname/SNI.
487    pub fn add_custom_ca_der(mut self, certificate: impl Into<Vec<u8>>) -> Self {
488        self.custom_ca_der.push(certificate.into());
489        self
490    }
491
492    pub async fn connect(self) -> Result<PulseClient> {
493        let started = tokio::time::Instant::now();
494        let host = endpoint_host(&self.endpoint)?;
495        let resolved = tokio::time::timeout(CONNECT_TIMEOUT, async {
496            let mut addresses: Vec<_> = tokio::net::lookup_host(self.endpoint.as_str())
497                .await
498                .map_err(|e| Error::Connect(format!("resolve {}: {e}", self.endpoint)))?
499                .collect();
500            // Prefer IPv4 when both families are returned. This avoids waiting
501            // out a full QUIC handshake timeout on systems where `localhost`
502            // (and some public resolvers) return an unreachable IPv6 address
503            // first. Stable sort preserves resolver order within each family.
504            addresses.sort_by_key(|address| !address.is_ipv4());
505            addresses.into_iter().next().ok_or_else(|| {
506                Error::Connect(format!("{} resolved to no addresses", self.endpoint))
507            })
508        })
509        .await
510        .map_err(|_| Error::ConnectTimeout)??;
511
512        let remaining = CONNECT_TIMEOUT
513            .checked_sub(started.elapsed())
514            .ok_or(Error::ConnectTimeout)?;
515
516        connect_to(
517            resolved,
518            host,
519            self.token,
520            Trust::Verified(self.custom_ca_der),
521            remaining,
522        )
523        .await
524    }
525}
526
527enum Trust {
528    Verified(Vec<Vec<u8>>),
529    InsecureLocalDev,
530}
531
532fn endpoint_host(endpoint: &str) -> Result<String> {
533    let endpoint = endpoint.trim();
534    if endpoint.is_empty() || endpoint.contains("://") || endpoint.contains('/') {
535        return Err(Error::InvalidEndpoint(
536            "expected host:port without a URL scheme or path".to_owned(),
537        ));
538    }
539    if let Ok(addr) = endpoint.parse::<SocketAddr>() {
540        return Ok(addr.ip().to_string());
541    }
542    let (host, port) = endpoint
543        .rsplit_once(':')
544        .ok_or_else(|| Error::InvalidEndpoint(format!("{endpoint:?} must include a port")))?;
545    if host.is_empty() || host.contains(':') || host.chars().any(char::is_whitespace) {
546        return Err(Error::InvalidEndpoint(format!(
547            "{endpoint:?} has an invalid host"
548        )));
549    }
550    port.parse::<u16>()
551        .map_err(|_| Error::InvalidEndpoint(format!("{endpoint:?} has an invalid port")))?;
552    Ok(host.trim_end_matches('.').to_owned())
553}
554
555async fn connect_to(
556    addr: SocketAddr,
557    server_name: String,
558    token: Option<String>,
559    trust: Trust,
560    timeout: Duration,
561) -> Result<PulseClient> {
562    if matches!(trust, Trust::InsecureLocalDev) && !addr.ip().is_loopback() {
563        return Err(Error::InsecureEndpointNotLoopback(addr));
564    }
565    let _ = rustls::crypto::ring::default_provider().install_default();
566
567    let mut tls = match trust {
568        Trust::Verified(custom_ca_der) => {
569            let mut roots = rustls::RootCertStore::empty();
570            let native = rustls_native_certs::load_native_certs();
571            for cert in native.certs {
572                roots
573                    .add(cert)
574                    .map_err(|e| Error::Tls(format!("invalid native trust anchor: {e}")))?;
575            }
576            for cert in custom_ca_der {
577                roots
578                    .add(rustls::pki_types::CertificateDer::from(cert))
579                    .map_err(|e| Error::Tls(format!("invalid custom CA certificate: {e}")))?;
580            }
581            if roots.is_empty() {
582                return Err(Error::Tls(
583                    "native certificate store contained no usable roots".to_owned(),
584                ));
585            }
586            rustls::ClientConfig::builder()
587                .with_root_certificates(roots)
588                .with_no_client_auth()
589        }
590        Trust::InsecureLocalDev => rustls::ClientConfig::builder()
591            .dangerous()
592            .with_custom_certificate_verifier(Arc::new(NoVerify))
593            .with_no_client_auth(),
594    };
595    tls.alpn_protocols = vec![thornode_pulse_wire::protocol::ALPN.to_vec()];
596
597    let qcc = quinn::crypto::rustls::QuicClientConfig::try_from(tls)
598        .map_err(|e| Error::Tls(e.to_string()))?;
599    let client_cfg = quinn::ClientConfig::new(Arc::new(qcc));
600
601    // Not Endpoint::client: that inherits net.core.rmem_default, which is far
602    // too small to absorb a shred-rate burst.
603    let bind_addr = SocketAddr::new(
604        if addr.is_ipv4() {
605            IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)
606        } else {
607            IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED)
608        },
609        0,
610    );
611    let socket =
612        client_socket(bind_addr, DEFAULT_RECV_BUFFER).map_err(|e| Error::Io(e.to_string()))?;
613    let mut endpoint = quinn::Endpoint::new(
614        quinn::EndpointConfig::default(),
615        None,
616        socket,
617        Arc::new(quinn::TokioRuntime),
618    )
619    .map_err(|e| Error::Io(e.to_string()))?;
620    endpoint.set_default_client_config(client_cfg);
621
622    let connecting = endpoint
623        .connect(addr, &server_name)
624        .map_err(|e| Error::Connect(e.to_string()))?;
625    let conn = tokio::time::timeout(timeout, connecting)
626        .await
627        .map_err(|_| Error::ConnectTimeout)?
628        .map_err(|error| {
629            close_info(&error)
630                .map(Error::ApplicationClosed)
631                .unwrap_or_else(|| Error::Connect(error.to_string()))
632        })?;
633
634    Ok(PulseClient {
635        conn,
636        _endpoint: endpoint,
637        token,
638    })
639}
640
641/// Returns the server's reason when a control message is rejected, and rejects
642/// an ack that names an unsupported wire version.
643///
644/// On the **sig-first tier the ack's `v` is the only version channel**: that
645/// tier is DATAGRAM-only, so there is no stream preamble. Without this check,
646/// a client could parse an 81-byte v2 datagram as the 72-byte v1 layout. The server closes
647/// (code 4) rather than acking a version it cannot serve, so in practice this
648/// is a backstop — but it is the only one this tier has.
649fn ensure_envelope(ack: &Ack) -> Result<()> {
650    let reason = ack.reason.clone().unwrap_or_default();
651    match ack.message_type.as_deref() {
652        Some("ack") => {
653            if ack.ok {
654                Ok(())
655            } else {
656                Err(Error::Rejected(reason))
657            }
658        }
659        Some("error") if !ack.ok => match ack.code {
660            Some(code) => Err(Error::ApplicationClosed(CloseInfo { code, reason })),
661            None => Err(Error::BadFrame),
662        },
663        // Missing/unknown envelope types, and a contradictory `error` success,
664        // are protocol errors rather than successful or ordinary rejections.
665        _ => Err(Error::BadFrame),
666    }
667}
668
669fn ensure_initial_ack(ack: &Ack) -> Result<()> {
670    ensure_envelope(ack)?;
671    match ack.v {
672        Some(v) if v == thornode_pulse_wire::frame::WIRE_VERSION as u32 => Ok(()),
673        Some(v) => Err(Error::VersionMismatch(v)),
674        None => Err(Error::MissingVersion),
675    }
676}
677
678fn ensure_update_ack(ack: &Ack) -> Result<()> {
679    ensure_envelope(ack)?;
680    // Updates normally omit `v`; if a peer does send one, it must not
681    // contradict the already-negotiated connection dialect.
682    match ack.v {
683        Some(v) if v != thornode_pulse_wire::frame::WIRE_VERSION as u32 => {
684            Err(Error::VersionMismatch(v))
685        }
686        _ => Ok(()),
687    }
688}
689
690/// Writes one control message (`"v"` always negotiates wire v2 —
691/// `thornode_pulse_wire::frame::WIRE_VERSION`) and reads back the server's ack
692/// envelope on the same stream. Shared by the initial subscribe and every
693/// later `update_filter` call.
694async fn control_round_trip(
695    conn: &quinn::Connection,
696    filter: &Filter,
697    full: bool,
698    fields: &[&str],
699    token: &str,
700) -> Result<Ack> {
701    let result = tokio::time::timeout(
702        ACK_TIMEOUT,
703        control_round_trip_inner(conn, filter, full, fields, token),
704    )
705    .await;
706    match result {
707        Ok(result) => result.map_err(|e| terminal_error(conn).unwrap_or(e)),
708        Err(_) => Err(terminal_error(conn).unwrap_or(Error::AckTimeout)),
709    }
710}
711
712async fn control_round_trip_inner(
713    conn: &quinn::Connection,
714    filter: &Filter,
715    full: bool,
716    fields: &[&str],
717    token: &str,
718) -> Result<Ack> {
719    let body = serde_json::to_vec(&Control {
720        filter,
721        token,
722        full,
723        v: thornode_pulse_wire::frame::WIRE_VERSION as u32,
724        fields,
725    })
726    .map_err(|_| Error::BadFrame)?;
727    let (mut send, mut recv) = conn.open_bi().await.map_err(|e| Error::Io(e.to_string()))?;
728    send.write_all(&body)
729        .await
730        .map_err(|e| Error::Io(e.to_string()))?;
731    let _ = send.finish();
732    read_ack(&mut recv).await
733}
734
735/// Bound on a control-ack envelope's length prefix: acks are a few dozen
736/// bytes of JSON, so this is generous headroom against a corrupted length
737/// rather than a realistic ack size.
738const MAX_ACK_BYTES: usize = 16 * 1024;
739
740/// How long the complete control round-trip (open, write and ack read) may
741/// take before failing with [`Error::AckTimeout`].
742///
743/// This bounds control-stream opening, writing, and acknowledgement reading.
744pub const ACK_TIMEOUT: Duration = Duration::from_secs(10);
745
746async fn read_ack(recv: &mut quinn::RecvStream) -> Result<Ack> {
747    let mut len = [0u8; 4];
748    recv.read_exact(&mut len)
749        .await
750        .map_err(|e| Error::Io(e.to_string()))?;
751    let n = u32::from_be_bytes(len) as usize;
752    if n > MAX_ACK_BYTES {
753        return Err(Error::BadFrame);
754    }
755    let mut body = vec![0u8; n];
756    recv.read_exact(&mut body)
757        .await
758        .map_err(|e| Error::Io(e.to_string()))?;
759    serde_json::from_slice(&body).map_err(|_| Error::BadFrame)
760}
761
762fn close_info(error: &quinn::ConnectionError) -> Option<CloseInfo> {
763    match error {
764        quinn::ConnectionError::ApplicationClosed(close) => Some(CloseInfo {
765            code: close.error_code.into_inner(),
766            reason: String::from_utf8_lossy(&close.reason).into_owned(),
767        }),
768        _ => None,
769    }
770}
771
772fn terminal_error(conn: &quinn::Connection) -> Option<Error> {
773    conn.close_reason().and_then(|error| {
774        close_info(&error)
775            .map(Error::ApplicationClosed)
776            .or_else(|| match error {
777                quinn::ConnectionError::LocallyClosed => None,
778                other => Some(Error::Io(other.to_string())),
779            })
780    })
781}
782
783/// Reads exactly [`thornode_pulse_wire::frame::PREAMBLE`]'s length from `recv` and
784/// rejects anything else — including a short read at EOF — as
785/// [`Error::BadPreamble`]. Generic over the reader so this is unit-testable
786/// against an in-memory pipe instead of requiring a live QUIC stream.
787async fn verify_preamble<R: tokio::io::AsyncRead + Unpin>(recv: &mut R) -> Result<()> {
788    let mut buf = [0u8; 6];
789    debug_assert_eq!(thornode_pulse_wire::frame::PREAMBLE.len(), buf.len());
790    recv.read_exact(&mut buf)
791        .await
792        .map_err(|_| Error::BadPreamble)?;
793    if &buf != thornode_pulse_wire::frame::PREAMBLE {
794        return Err(Error::BadPreamble);
795    }
796    Ok(())
797}
798
799/// One sig-first delivery: the transaction's slot, this subscriber's
800/// per-connection sequence number (see [`SigFirstSub::gaps`]), and its
801/// signature.
802#[derive(Debug, Clone, Copy, PartialEq, Eq)]
803pub struct SigFirstItem {
804    pub slot: u64,
805    pub seq: u64,
806    pub signature: [u8; 64],
807}
808
809/// Folds an item's own `seq` into the running (last-seen, gap-count) state. A
810/// gap is exactly the count of sequence numbers skipped between the previous
811/// (highest-seen) item this subscriber saw and this one.
812///
813/// QUIC DATAGRAMs are explicitly unordered, so out-of-order arrival is
814/// expected traffic, not a pathology — the watermark (`*last_seq`) MUST be
815/// monotonic (`last.max(seq)`), never just overwritten with whatever arrived
816/// most recently. An unconditional overwrite would let a reordered item drag
817/// the watermark backwards, and the very next in-order item would then be
818/// charged again for a range that was never actually missing — inflating
819/// `gaps()`, this tier's only loss signal, on a stream that lost nothing.
820fn note_item_seq(last_seq: &mut Option<u64>, gaps: &AtomicU64, seq: u64) {
821    if let Some(last) = *last_seq {
822        // `last.saturating_add(1)`, not `last + 1`: a corrupt or hostile
823        // datagram could carry `seq == NO_SEQ_ASSIGNED` (u64::MAX) as a real
824        // item seq — `apply_datagram` has no reason to reject that value on
825        // this path (the sentinel is only reserved on the HEARTBEAT side) —
826        // and an unguarded `+ 1` would overflow-panic the spawned drain task
827        // in a debug build, surfacing to the caller as a silent `Ok(None)`
828        // indistinguishable from a clean server close.
829        gaps.fetch_add(
830            seq.saturating_sub(last.saturating_add(1)),
831            Ordering::Relaxed,
832        );
833        *last_seq = Some(last.max(seq));
834    } else {
835        *last_seq = Some(seq);
836    }
837}
838
839/// Folds a heartbeat's `highest_seq` into the running (last-seen, gap-count)
840/// state. This is what reveals TRAILING loss — datagrams dropped after the
841/// last item this subscriber actually received, which item-to-item
842/// comparison alone can never see (there is no next item to reveal the hole).
843///
844/// [`NO_SEQ_ASSIGNED`] MUST be treated as "no information yet", never as a
845/// real, enormous sequence number: a naive `highest_seq - last` on the
846/// sentinel would report an absurd multi-quintillion gap instead of the true
847/// answer, which is "nothing assigned yet, so don't guess".
848fn note_heartbeat_seq(last_seq: &mut Option<u64>, gaps: &AtomicU64, highest_seq: u64) {
849    if highest_seq == NO_SEQ_ASSIGNED {
850        return;
851    }
852    match *last_seq {
853        Some(last) if highest_seq > last => {
854            gaps.fetch_add(highest_seq - last, Ordering::Relaxed);
855            *last_seq = Some(highest_seq);
856        }
857        // Heartbeat is stale/equal to what item traffic already told us:
858        // nothing new to fold in.
859        Some(_) => {}
860        // First observation ever, with no item to compare against: establish
861        // a baseline rather than alleging a gap we have no evidence for.
862        None => *last_seq = Some(highest_seq),
863    }
864}
865
866/// Applies one decoded datagram to the running gap-tracking state, returning
867/// the item to forward (if any). `Datagram::Unknown` and a `None` decode
868/// (corrupt bytes, or a known type too short to parse) both mean "skip" —
869/// never an error, never a reason to tear the stream down; that is what
870/// keeps a future datagram type from breaking this client.
871/// `Datagram::Heartbeat` updates the gap counter but is never forwarded as an
872/// item.
873fn apply_datagram(dg: &[u8], last_seq: &mut Option<u64>, gaps: &AtomicU64) -> Option<SigFirstItem> {
874    match decode_datagram(dg) {
875        Some(Datagram::SigFirst {
876            slot,
877            seq,
878            signature,
879        }) => {
880            note_item_seq(last_seq, gaps, seq);
881            Some(SigFirstItem {
882                slot,
883                seq,
884                signature,
885            })
886        }
887        Some(Datagram::Heartbeat { highest_seq, .. }) => {
888            note_heartbeat_seq(last_seq, gaps, highest_seq);
889            None
890        }
891        Some(Datagram::Unknown(_)) | None => None,
892    }
893}
894
895/// Depth of the sig-first handoff queue.
896///
897/// quinn buffers only a small budget of datagrams itself and evicts on
898/// overflow, so the SDK drains it continuously into this queue rather than
899/// leaving datagrams there until the caller happens to call
900/// [`SigFirstSub::next`].
901pub const SIG_QUEUE_LEN: usize = 4096;
902
903/// Live sig-first subscription. Call [`SigFirstSub::next`] in a loop.
904///
905/// A background task drains the connection as fast as the network delivers.
906/// When a slow `next` loop fills the queue, the oldest items are evicted and
907/// counted by [`SigFirstSub::dropped`]. [`SigFirstSub::gaps`] is a separate,
908/// provisional counter for
909/// sequence numbers that appear not to have arrived (network loss, a shed
910/// delivery — or merely reordering, which it cannot tell apart), as opposed to
911/// `dropped`, which counts items that definitely arrived and were evicted
912/// because this consumer fell behind.
913pub struct SigFirstSub {
914    conn: Option<quinn::Connection>,
915    rx: tokio::sync::broadcast::Receiver<SigFirstItem>,
916    dropped: Arc<AtomicU64>,
917    gaps: Arc<AtomicU64>,
918    /// Terminal error, if the drain ended on anything but a clean close.
919    fatal: Arc<OnceLock<Error>>,
920    drain: Option<tokio::task::JoinHandle<()>>,
921}
922
923impl SigFirstSub {
924    fn spawn(conn: quinn::Connection) -> Self {
925        let (tx, rx) = tokio::sync::broadcast::channel(SIG_QUEUE_LEN);
926        let dropped = Arc::new(AtomicU64::new(0));
927        let gaps = Arc::new(AtomicU64::new(0));
928        let fatal: Arc<OnceLock<Error>> = Arc::new(OnceLock::new());
929
930        let drain_conn = conn.clone();
931        let drain_fatal = Arc::clone(&fatal);
932        let drain_gaps = Arc::clone(&gaps);
933        let drain = tokio::spawn(async move {
934            let mut last_seq: Option<u64> = None;
935            loop {
936                match drain_conn.read_datagram().await {
937                    Ok(dg) => {
938                        if let Some(item) = apply_datagram(&dg, &mut last_seq, &drain_gaps) {
939                            // A broadcast send only fails when every receiver
940                            // is gone, which means the caller dropped the sub.
941                            if tx.send(item).is_err() {
942                                return;
943                            }
944                        }
945                    }
946                    Err(quinn::ConnectionError::LocallyClosed) => return,
947                    Err(e) => {
948                        let error = close_info(&e)
949                            .map(Error::ApplicationClosed)
950                            .unwrap_or_else(|| Error::Io(e.to_string()));
951                        let _ = drain_fatal.set(error);
952                        return;
953                    }
954                }
955            }
956        });
957
958        SigFirstSub {
959            conn: Some(conn),
960            rx,
961            dropped,
962            gaps,
963            fatal,
964            drain: Some(drain),
965        }
966    }
967
968    #[cfg(test)]
969    fn for_test(rx: tokio::sync::broadcast::Receiver<SigFirstItem>) -> Self {
970        SigFirstSub {
971            conn: None,
972            rx,
973            dropped: Arc::new(AtomicU64::new(0)),
974            gaps: Arc::new(AtomicU64::new(0)),
975            fatal: Arc::new(OnceLock::new()),
976            drain: None,
977        }
978    }
979
980    /// Awaits the next [`SigFirstItem`]. A QUIC application close is returned
981    /// as [`Error::ApplicationClosed`], including normal close code `0`;
982    /// `Ok(None)` is reserved for a locally ended drain with no terminal error.
983    ///
984    /// Do the work for each item elsewhere. Time spent between two calls is
985    /// queue depth, and past [`SIG_QUEUE_LEN`] it is loss.
986    pub async fn next(&mut self) -> Result<Option<SigFirstItem>> {
987        loop {
988            match self.rx.recv().await {
989                Ok(item) => return Ok(Some(item)),
990                // The queue overran while we were away: it kept the newest and
991                // tells us exactly how many it discarded.
992                Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
993                    self.dropped.fetch_add(n, Ordering::Relaxed);
994                }
995                Err(tokio::sync::broadcast::error::RecvError::Closed) => {
996                    return match self.fatal.get() {
997                        Some(e) => Err(e.clone()),
998                        None => Ok(None),
999                    };
1000                }
1001            }
1002        }
1003    }
1004
1005    /// Items evicted because this consumer fell behind. Watch it: no kernel
1006    /// or NIC counter will show this loss.
1007    pub fn dropped(&self) -> u64 {
1008        self.dropped.load(Ordering::Relaxed)
1009    }
1010
1011    /// A **provisional** loss indicator: item-to-item `seq` gaps plus
1012    /// trailing loss revealed by a heartbeat's `highest_seq`.
1013    /// [`NO_SEQ_ASSIGNED`] on the wire never contributes to this counter.
1014    ///
1015    /// It can **over-report under reordering**. QUIC DATAGRAMs are unordered
1016    /// by definition, so a scalar high-watermark cannot distinguish "this seq
1017    /// is late" from "this seq is lost" at the moment a later one arrives out
1018    /// of order — it charges one provisional gap on that jump, and never
1019    /// reverses the charge if the late item shows up afterward. A perfectly
1020    /// lossless but reordered stream can therefore report `gaps() > 0`. Treat
1021    /// this as "loss happened, or reordering did" rather than an exact count
1022    /// of sequence numbers that never arrived on the wire at all.
1023    pub fn gaps(&self) -> u64 {
1024        self.gaps.load(Ordering::Relaxed)
1025    }
1026
1027    /// Updates the active filter live (opens a fresh control stream) and
1028    /// returns the server's parsed ack. Enrichment fields do not exist on the
1029    /// sig-first tier, so this always sends an empty `fields` list. The tier
1030    /// cannot change after the first control message.
1031    pub async fn update_filter(&self, filter: &Filter) -> Result<Ack> {
1032        let ack = match &self.conn {
1033            Some(conn) => control_round_trip(conn, filter, false, &[], "").await,
1034            // `#[cfg(test)] for_test` variant: no real connection to update.
1035            None => Ok(Ack {
1036                message_type: Some("ack".to_owned()),
1037                ok: true,
1038                reason: None,
1039                code: None,
1040                v: None,
1041            }),
1042        }?;
1043        ensure_update_ack(&ack)?;
1044        Ok(ack)
1045    }
1046}
1047
1048impl Drop for SigFirstSub {
1049    fn drop(&mut self) {
1050        if let Some(drain) = self.drain.take() {
1051            drain.abort();
1052        }
1053    }
1054}
1055
1056/// Sanity cap on a single v2 frame's total length prefix — generous enough
1057/// for `MAX_FULL_TX_BODY` plus the largest possible TLV trailer (two
1058/// loaded-address lists, each up to `u16::MAX` bytes long) plus the 2-byte
1059/// msg_type/flags header. A plain v1-sized cap here would wrongly reject a
1060/// legitimately large `fields: ["alt"]`-enriched frame.
1061const MAX_FULL_TX_FRAME: usize =
1062    thornode_pulse_wire::frame::MAX_FULL_TX_BODY + 2 * (u16::MAX as usize + 3) + 2;
1063
1064/// Live full-tx subscription. Call [`FullSub::next`] in a loop.
1065pub struct FullSub {
1066    conn: quinn::Connection,
1067    recv: quinn::RecvStream,
1068    buf: Vec<u8>,
1069    /// The most recent heartbeat observed on this stream: `(server_ts_ms,
1070    /// highest_seq)`. See [`FullSub::heartbeat`].
1071    last_heartbeat: Option<(u64, u64)>,
1072}
1073
1074impl FullSub {
1075    /// Awaits the next transaction frame. `Frame::Unknown` message types are
1076    /// skipped transparently for forward compatibility, and
1077    /// `Frame::Heartbeat` frames update [`FullSub::heartbeat`] instead of
1078    /// being returned. Only `Frame::Tx` is ever handed back here. Returns
1079    /// `Ok(None)` at a clean end of stream.
1080    pub async fn next(&mut self) -> Result<Option<Frame>> {
1081        match next_frame(&mut self.recv, &mut self.buf, &mut self.last_heartbeat).await {
1082            Ok(None) => match terminal_error(&self.conn) {
1083                Some(error) => Err(error),
1084                None => Ok(None),
1085            },
1086            Err(error) => Err(merge_wire_and_terminal(error, terminal_error(&self.conn))),
1087            ok => ok,
1088        }
1089    }
1090
1091    /// The most recent heartbeat observed on this stream: `(server_ts_ms,
1092    /// highest_seq)`. `highest_seq == NO_SEQ_ASSIGNED` means the server has
1093    /// not assigned this subscriber a transaction yet. `None` means no
1094    /// heartbeat has arrived at all yet (a busy stream can go a long time
1095    /// without one — the server resets its heartbeat timer on every real
1096    /// send).
1097    ///
1098    /// Unlike [`SigFirstSub::gaps`], the full-tx wire carries no per-frame
1099    /// sequence number, so this SDK cannot compute a numeric gap count for
1100    /// this tier — `highest_seq` is the raw signal a caller can compare
1101    /// against its own received-frame count if it wants that.
1102    pub fn heartbeat(&self) -> Option<(u64, u64)> {
1103        self.last_heartbeat
1104    }
1105
1106    /// Updates the active filter/enrichment fields live (opens a fresh
1107    /// control stream) and returns the server's parsed ack. The tier cannot
1108    /// change after the first control message.
1109    pub async fn update_filter(&self, filter: &Filter, fields: &[&str]) -> Result<Ack> {
1110        let ack = control_round_trip(&self.conn, filter, true, fields, "").await?;
1111        ensure_update_ack(&ack)?;
1112        Ok(ack)
1113    }
1114}
1115
1116/// Reads and decodes the next `Frame` from `recv`, transparently skipping
1117/// `Frame::Unknown` and folding `Frame::Heartbeat` into `*last_heartbeat`
1118/// until a `Frame::Tx` arrives or the stream ends cleanly. Generic over the
1119/// reader so this framing logic is unit-testable against an in-memory pipe
1120/// instead of requiring a live QUIC stream — see the `next_frame_*` tests
1121/// below.
1122async fn next_frame<R: tokio::io::AsyncRead + Unpin>(
1123    recv: &mut R,
1124    buf: &mut Vec<u8>,
1125    last_heartbeat: &mut Option<(u64, u64)>,
1126) -> Result<Option<Frame>> {
1127    loop {
1128        // Each frame is a u32 big-endian length prefix followed by the body.
1129        let len = match read_n_or_eof(recv, buf, 4).await? {
1130            Some(()) => {
1131                let l = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
1132                if l > MAX_FULL_TX_FRAME {
1133                    return Err(Error::BadFrame);
1134                }
1135                l
1136            }
1137            None => return Ok(None),
1138        };
1139        match read_n_or_eof(recv, buf, len).await {
1140            Ok(Some(())) => match decode_frame(&buf[..len]) {
1141                Ok(Frame::Unknown(_)) => continue,
1142                Ok(Frame::Heartbeat {
1143                    server_ts_ms,
1144                    highest_seq,
1145                }) => {
1146                    *last_heartbeat = Some((server_ts_ms, highest_seq));
1147                    continue;
1148                }
1149                Ok(tx @ Frame::Tx(_)) => return Ok(Some(tx)),
1150                Err(_) => return Err(Error::BadFrame),
1151            },
1152            // The length prefix arrived but the body never did. That is a
1153            // TRUNCATED frame, not a clean close — the sender told us how many
1154            // bytes were coming and then stopped. Reporting `Ok(None)` here
1155            // would present silent loss as a normal end of stream. Matches
1156            // Go's `nextFrame` (`ErrBadFrame`); a clean close is only a close
1157            // that happens on a frame boundary, i.e. before the length prefix.
1158            Ok(None) | Err(_) => return Err(Error::BadFrame),
1159        }
1160    }
1161}
1162
1163/// Fills `buf[..n]` with exactly `n` bytes. Returns `Ok(None)` on a clean
1164/// end-of-stream before any byte was read; a partial read followed by EOF is
1165/// `Error::BadFrame` (a truncated frame, not a clean boundary).
1166async fn read_n_or_eof<R: tokio::io::AsyncRead + Unpin>(
1167    recv: &mut R,
1168    buf: &mut Vec<u8>,
1169    n: usize,
1170) -> Result<Option<()>> {
1171    buf.resize(n, 0);
1172    let mut got = 0;
1173    while got < n {
1174        match recv.read(&mut buf[got..n]).await {
1175            Ok(0) => {
1176                return if got == 0 {
1177                    Ok(None)
1178                } else {
1179                    Err(Error::BadFrame)
1180                };
1181            }
1182            Ok(k) => got += k,
1183            Err(error) if got == 0 => return Err(Error::Io(error.to_string())),
1184            Err(_) => return Err(Error::BadFrame),
1185        }
1186    }
1187    Ok(Some(()))
1188}
1189
1190fn merge_wire_and_terminal(error: Error, terminal: Option<Error>) -> Error {
1191    match (error, terminal) {
1192        (Error::BadFrame, Some(Error::ApplicationClosed(close))) => Error::BadFrameWithClose(close),
1193        (Error::BadPreamble, Some(Error::ApplicationClosed(close))) => {
1194            Error::BadPreambleWithClose(close)
1195        }
1196        (_, Some(terminal)) => terminal,
1197        (error, None) => error,
1198    }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203    use super::*;
1204
1205    // ---- SigFirstSub: consumer backpressure (dropped) ----------------------
1206
1207    /// A stalled consumer must lose the OLDEST items and be able to count
1208    /// them. Nothing below the SDK will ever report this loss.
1209    #[tokio::test]
1210    async fn stalled_consumer_loses_oldest_and_counts_them() {
1211        let (tx, rx) = tokio::sync::broadcast::channel(4);
1212        let mut sub = SigFirstSub::for_test(rx);
1213
1214        for slot in 0..10u64 {
1215            tx.send(SigFirstItem {
1216                slot,
1217                seq: slot,
1218                signature: [0u8; 64],
1219            })
1220            .unwrap();
1221        }
1222        drop(tx); // end of stream once the buffered items are read
1223
1224        let mut got = Vec::new();
1225        while let Some(item) = sub.next().await.unwrap() {
1226            got.push(item.slot);
1227        }
1228
1229        assert_eq!(got, vec![6, 7, 8, 9], "the freshest items must survive");
1230        assert_eq!(sub.dropped(), 6);
1231    }
1232
1233    /// A consumer that keeps up loses nothing and counts nothing.
1234    #[tokio::test]
1235    async fn consumer_that_keeps_up_drops_nothing() {
1236        let (tx, rx) = tokio::sync::broadcast::channel(4);
1237        let mut sub = SigFirstSub::for_test(rx);
1238
1239        tx.send(SigFirstItem {
1240            slot: 1,
1241            seq: 0,
1242            signature: [0u8; 64],
1243        })
1244        .unwrap();
1245        tx.send(SigFirstItem {
1246            slot: 2,
1247            seq: 1,
1248            signature: [0u8; 64],
1249        })
1250        .unwrap();
1251        drop(tx);
1252
1253        let mut got = Vec::new();
1254        while let Some(item) = sub.next().await.unwrap() {
1255            got.push(item.slot);
1256        }
1257
1258        assert_eq!(got, vec![1, 2]);
1259        assert_eq!(sub.dropped(), 0);
1260    }
1261
1262    // ---- gap tracking: the u64::MAX sentinel must never fabricate a gap ----
1263
1264    #[test]
1265    fn note_item_seq_counts_missed_numbers_between_consecutive_items() {
1266        let mut last_seq = None;
1267        let gaps = AtomicU64::new(0);
1268        note_item_seq(&mut last_seq, &gaps, 0);
1269        assert_eq!(
1270            gaps.load(Ordering::Relaxed),
1271            0,
1272            "first item establishes the baseline"
1273        );
1274        note_item_seq(&mut last_seq, &gaps, 3); // missed 1 and 2
1275        assert_eq!(gaps.load(Ordering::Relaxed), 2);
1276        assert_eq!(last_seq, Some(3));
1277    }
1278
1279    #[test]
1280    fn note_item_seq_out_of_order_never_underflows() {
1281        // Datagrams are UDP: they can arrive out of order. A later item with a
1282        // LOWER seq than the last one seen must not wrap a u64 subtraction.
1283        let mut last_seq = Some(10u64);
1284        let gaps = AtomicU64::new(0);
1285        note_item_seq(&mut last_seq, &gaps, 3);
1286        assert_eq!(
1287            gaps.load(Ordering::Relaxed),
1288            0,
1289            "no underflow, no bogus gap"
1290        );
1291        // The watermark must stay monotonic: a reordered item behind the high
1292        // watermark must never drag it backwards (see
1293        // `note_item_seq_reordering_does_not_double_count_the_same_gap` for
1294        // why that matters — a regressed watermark double-charges the next
1295        // in-order item for a range that was never actually missing).
1296        assert_eq!(last_seq, Some(10), "watermark must not regress on reorder");
1297        note_item_seq(&mut last_seq, &gaps, 11);
1298        assert_eq!(
1299            gaps.load(Ordering::Relaxed),
1300            0,
1301            "seq 11 directly follows the watermark of 10"
1302        );
1303        assert_eq!(last_seq, Some(11));
1304    }
1305
1306    #[test]
1307    fn note_item_seq_reordering_does_not_double_count_the_same_gap() {
1308        // Sequences 0,1,2,3 can arrive as 0,2,1,3 with no actual loss. The
1309        // 0->2 jump produces one provisional gap, but the late 1 must not move
1310        // the watermark backwards and cause the following 3 to count it again.
1311        let mut last_seq = None;
1312        let gaps = AtomicU64::new(0);
1313        for seq in [0u64, 2, 1, 3] {
1314            note_item_seq(&mut last_seq, &gaps, seq);
1315        }
1316        assert_eq!(
1317            gaps.load(Ordering::Relaxed),
1318            1,
1319            "one provisional gap from the 0->2 jump, never double-charged on the later in-order 3"
1320        );
1321        assert_eq!(
1322            last_seq,
1323            Some(3),
1324            "watermark must track the highest seq seen, not the latest arrival"
1325        );
1326    }
1327
1328    #[test]
1329    fn note_item_seq_sentinel_seq_does_not_overflow_the_gap_addition() {
1330        // A corrupt or hostile datagram could carry seq == u64::MAX. The `+1`
1331        // this function computes against the PREVIOUS watermark must not
1332        // panic (debug-build overflow) — that would kill the drain task and
1333        // surface to the caller as an indistinguishable-from-clean `Ok(None)`.
1334        let mut last_seq = Some(u64::MAX);
1335        let gaps = AtomicU64::new(0);
1336        note_item_seq(&mut last_seq, &gaps, u64::MAX);
1337        assert_eq!(gaps.load(Ordering::Relaxed), 0);
1338        assert_eq!(last_seq, Some(u64::MAX));
1339    }
1340
1341    #[test]
1342    fn note_heartbeat_seq_sentinel_is_never_a_gap() {
1343        // THE required property: NO_SEQ_ASSIGNED (u64::MAX) must never be
1344        // treated as a real value. A naive `highest_seq - last` here would
1345        // compute an astronomical, nonsensical gap.
1346        let mut last_seq = Some(5u64);
1347        let gaps = AtomicU64::new(0);
1348        note_heartbeat_seq(&mut last_seq, &gaps, NO_SEQ_ASSIGNED);
1349        assert_eq!(gaps.load(Ordering::Relaxed), 0);
1350        assert_eq!(
1351            last_seq,
1352            Some(5),
1353            "the sentinel must not overwrite a real baseline either"
1354        );
1355    }
1356
1357    #[test]
1358    fn note_heartbeat_seq_reveals_trailing_loss() {
1359        // This is the case item-to-item comparison can never see: datagrams
1360        // dropped AFTER the last one we actually received, with nothing since
1361        // to reveal the hole. Only a heartbeat's highest_seq can tell us.
1362        let mut last_seq = Some(2u64);
1363        let gaps = AtomicU64::new(0);
1364        note_heartbeat_seq(&mut last_seq, &gaps, 7);
1365        assert_eq!(gaps.load(Ordering::Relaxed), 5);
1366        assert_eq!(last_seq, Some(7));
1367    }
1368
1369    #[test]
1370    fn note_heartbeat_seq_first_observation_establishes_a_baseline_not_a_gap() {
1371        // No prior item to compare against: we have no evidence anything was
1372        // actually lost, so don't allege a number we can't justify.
1373        let mut last_seq = None;
1374        let gaps = AtomicU64::new(0);
1375        note_heartbeat_seq(&mut last_seq, &gaps, 9);
1376        assert_eq!(gaps.load(Ordering::Relaxed), 0);
1377        assert_eq!(last_seq, Some(9));
1378    }
1379
1380    // ---- apply_datagram: unknown types are skipped, never an error ---------
1381
1382    #[test]
1383    fn apply_datagram_skips_an_unknown_type() {
1384        let mut last_seq = None;
1385        let gaps = AtomicU64::new(0);
1386        let buf = [200u8, 1, 2, 3];
1387        assert_eq!(apply_datagram(&buf, &mut last_seq, &gaps), None);
1388        assert_eq!(gaps.load(Ordering::Relaxed), 0);
1389    }
1390
1391    #[test]
1392    fn apply_datagram_forwards_sig_first_and_tracks_gaps() {
1393        let mut last_seq = None;
1394        let gaps = AtomicU64::new(0);
1395        let mut buf = [0u8; thornode_pulse_wire::frame::DG_SIG_FIRST_MIN];
1396
1397        thornode_pulse_wire::frame::encode_dg_sig_first(&mut buf, 100, 0, &[1u8; 64]);
1398        let item = apply_datagram(&buf, &mut last_seq, &gaps).expect("sig-first forwards");
1399        assert_eq!((item.slot, item.seq), (100, 0));
1400
1401        thornode_pulse_wire::frame::encode_dg_sig_first(&mut buf, 100, 3, &[1u8; 64]);
1402        let item = apply_datagram(&buf, &mut last_seq, &gaps).expect("sig-first forwards");
1403        assert_eq!(item.seq, 3);
1404        assert_eq!(gaps.load(Ordering::Relaxed), 2, "missed seq 1 and 2");
1405    }
1406
1407    #[test]
1408    fn apply_datagram_heartbeat_is_never_forwarded_as_an_item() {
1409        let mut last_seq = Some(1u64);
1410        let gaps = AtomicU64::new(0);
1411        let mut buf = [0u8; thornode_pulse_wire::frame::DG_HEARTBEAT_MIN];
1412        thornode_pulse_wire::frame::encode_dg_heartbeat(&mut buf, 123, 4);
1413        assert_eq!(apply_datagram(&buf, &mut last_seq, &gaps), None);
1414        assert_eq!(gaps.load(Ordering::Relaxed), 3);
1415    }
1416
1417    // ---- next_frame: unknown frames skipped, heartbeats folded, not items --
1418
1419    fn sample_full_tx() -> thornode_pulse_wire::frame::FullTx {
1420        thornode_pulse_wire::frame::FullTx {
1421            slot: 438_690_000,
1422            versioned: false,
1423            num_required_signatures: 1,
1424            num_readonly_signed_accounts: 0,
1425            num_readonly_unsigned_accounts: 0,
1426            recent_blockhash: [0xCC; 32],
1427            signatures: vec![[7u8; 64]],
1428            account_keys: vec![[0xA1; 32]],
1429            instructions: vec![thornode_pulse_wire::frame::FullInstruction {
1430                program_id_index: 0,
1431                accounts: vec![],
1432                data: vec![9, 9],
1433            }],
1434            address_table_lookups: vec![],
1435        }
1436    }
1437
1438    async fn write_framed(w: &mut (impl tokio::io::AsyncWrite + Unpin), body: &[u8]) {
1439        use tokio::io::AsyncWriteExt;
1440        w.write_all(&(body.len() as u32).to_be_bytes())
1441            .await
1442            .unwrap();
1443        w.write_all(body).await.unwrap();
1444    }
1445
1446    #[tokio::test]
1447    async fn next_frame_skips_unknown_and_folds_heartbeat_without_surfacing_it() {
1448        let (mut writer, mut reader) = tokio::io::duplex(4096);
1449
1450        // Frame 1: an unknown message type (99) — must be skipped, not error.
1451        write_framed(&mut writer, &[99u8, 0]).await;
1452
1453        // Frame 2: a heartbeat — must update `last_heartbeat`, not be returned.
1454        let mut hb = Vec::new();
1455        hb.push(thornode_pulse_wire::frame::MSG_HEARTBEAT);
1456        hb.push(0);
1457        thornode_pulse_wire::frame::put_tlv(
1458            &mut hb,
1459            thornode_pulse_wire::frame::TLV_SERVER_TS_MS,
1460            &123u64.to_le_bytes(),
1461        );
1462        thornode_pulse_wire::frame::put_tlv(
1463            &mut hb,
1464            thornode_pulse_wire::frame::TLV_HIGHEST_SEQ,
1465            &7u64.to_le_bytes(),
1466        );
1467        write_framed(&mut writer, &hb).await;
1468
1469        // Frame 3: a real (bare) tx frame — what next_frame must finally return.
1470        let tx = sample_full_tx();
1471        let tx_bytes = thornode_pulse_wire::frame::encode_frame_tx(&tx, false, &[], &[]);
1472        write_framed(&mut writer, &tx_bytes).await;
1473        drop(writer); // clean EOF right after
1474
1475        let mut buf = Vec::new();
1476        let mut last_heartbeat = None;
1477        let got = next_frame(&mut reader, &mut buf, &mut last_heartbeat)
1478            .await
1479            .unwrap();
1480        match got {
1481            Some(Frame::Tx(v2)) => assert_eq!(v2.tx, tx),
1482            other => panic!("expected Some(Frame::Tx(_)), got {other:?}"),
1483        }
1484        assert_eq!(
1485            last_heartbeat,
1486            Some((123, 7)),
1487            "the heartbeat must be captured via the accessor, not returned as an item"
1488        );
1489    }
1490
1491    #[tokio::test]
1492    async fn next_frame_returns_none_at_a_clean_end_of_stream() {
1493        let (writer, mut reader) = tokio::io::duplex(64);
1494        drop(writer);
1495        let mut buf = Vec::new();
1496        let mut last_heartbeat = None;
1497        assert_eq!(
1498            next_frame(&mut reader, &mut buf, &mut last_heartbeat)
1499                .await
1500                .unwrap(),
1501            None
1502        );
1503    }
1504
1505    /// A frame whose length prefix arrived but whose body never did is
1506    /// truncated, not a clean close.
1507    #[tokio::test]
1508    async fn next_frame_rejects_a_length_prefix_with_no_body() {
1509        for body_bytes in [0usize, 3] {
1510            let (mut writer, mut reader) = tokio::io::duplex(64);
1511            {
1512                use tokio::io::AsyncWriteExt;
1513                writer.write_all(&64u32.to_be_bytes()).await.unwrap();
1514                if body_bytes > 0 {
1515                    writer.write_all(&vec![0u8; body_bytes]).await.unwrap();
1516                }
1517            }
1518            drop(writer); // EOF mid-frame
1519            let mut buf = Vec::new();
1520            let mut last_heartbeat = None;
1521            let err = next_frame(&mut reader, &mut buf, &mut last_heartbeat)
1522                .await
1523                .unwrap_err();
1524            assert!(
1525                matches!(err, Error::BadFrame),
1526                "a 64-byte frame truncated to {body_bytes} body bytes must be BadFrame, got {err:?}"
1527            );
1528        }
1529    }
1530
1531    /// The complement: a close on a frame BOUNDARY (a partial length prefix
1532    /// counts as mid-frame too) stays a clean end of stream only when nothing
1533    /// at all was pending.
1534    #[tokio::test]
1535    async fn next_frame_rejects_a_partial_length_prefix() {
1536        let (mut writer, mut reader) = tokio::io::duplex(64);
1537        {
1538            use tokio::io::AsyncWriteExt;
1539            writer.write_all(&[0u8, 0, 1]).await.unwrap(); // 3 of 4 prefix bytes
1540        }
1541        drop(writer);
1542        let mut buf = Vec::new();
1543        let mut last_heartbeat = None;
1544        let err = next_frame(&mut reader, &mut buf, &mut last_heartbeat)
1545            .await
1546            .unwrap_err();
1547        assert!(matches!(err, Error::BadFrame), "got {err:?}");
1548    }
1549
1550    #[test]
1551    fn truncated_frame_preserves_a_simultaneous_application_close() {
1552        let close = CloseInfo {
1553            code: 3,
1554            reason: "capacity temporarily unavailable".to_owned(),
1555        };
1556        let error = merge_wire_and_terminal(
1557            Error::BadFrame,
1558            Some(Error::ApplicationClosed(close.clone())),
1559        );
1560        assert!(error.is_bad_frame());
1561        assert_eq!(error.close_info(), Some(&close));
1562        assert!(matches!(error, Error::BadFrameWithClose(_)));
1563    }
1564
1565    // ---- preamble: loud failure, never a silent skip ------------------------
1566
1567    #[tokio::test]
1568    async fn verify_preamble_accepts_the_real_preamble() {
1569        let (mut writer, mut reader) = tokio::io::duplex(64);
1570        tokio::spawn(async move {
1571            use tokio::io::AsyncWriteExt;
1572            let _ = writer.write_all(thornode_pulse_wire::frame::PREAMBLE).await;
1573        });
1574        verify_preamble(&mut reader).await.unwrap();
1575    }
1576
1577    #[tokio::test]
1578    async fn verify_preamble_rejects_a_mismatched_header_loudly() {
1579        let (mut writer, mut reader) = tokio::io::duplex(64);
1580        tokio::spawn(async move {
1581            use tokio::io::AsyncWriteExt;
1582            let _ = writer.write_all(b"XXXXXX").await;
1583        });
1584        let err = verify_preamble(&mut reader).await.unwrap_err();
1585        assert!(matches!(err, Error::BadPreamble), "got {err:?}");
1586    }
1587
1588    #[tokio::test]
1589    async fn verify_preamble_rejects_a_short_stream_loudly() {
1590        let (writer, mut reader) = tokio::io::duplex(64);
1591        drop(writer); // EOF before the 6-byte preamble is complete
1592        let err = verify_preamble(&mut reader).await.unwrap_err();
1593        assert!(matches!(err, Error::BadPreamble), "got {err:?}");
1594    }
1595
1596    // ---- control-ack interpretation ---------------------------------------
1597
1598    fn parse_ack(json: &str) -> Ack {
1599        serde_json::from_str(json).expect("ack envelope must deserialize")
1600    }
1601
1602    /// The ack's `v` is the sig-first tier's ONLY version channel — no
1603    /// stream, so no preamble. A client that ignores it proceeds against a
1604    /// v1 server and misparses every datagram.
1605    #[test]
1606    fn an_ack_negotiating_an_older_version_is_rejected() {
1607        let err = ensure_initial_ack(&parse_ack(r#"{"type":"ack","ok":true,"v":1}"#)).unwrap_err();
1608        match err {
1609            Error::VersionMismatch(v) => assert_eq!(v, 1),
1610            other => panic!("expected VersionMismatch, got {other:?}"),
1611        }
1612    }
1613
1614    #[test]
1615    fn initial_ack_requires_v_but_update_ack_may_omit_it() {
1616        ensure_initial_ack(&parse_ack(r#"{"type":"ack","ok":true,"v":2}"#))
1617            .expect("v2 is what we speak");
1618        let missing = parse_ack(r#"{"type":"ack","ok":true}"#);
1619        assert_eq!(ensure_initial_ack(&missing), Err(Error::MissingVersion));
1620        ensure_update_ack(&missing).expect("an update ack intentionally omits v");
1621        ensure_update_ack(&parse_ack(r#"{"type":"ack","ok":true,"v":2}"#))
1622            .expect("a matching additive update version is harmless");
1623        assert_eq!(
1624            ensure_update_ack(&parse_ack(r#"{"type":"ack","ok":true,"v":3}"#)),
1625            Err(Error::VersionMismatch(3))
1626        );
1627    }
1628
1629    #[test]
1630    fn ack_envelope_requires_a_known_type() {
1631        for json in [
1632            r#"{"ok":true,"v":2}"#,
1633            r#"{"type":"future","ok":true,"v":2}"#,
1634            r#"{"type":"error","ok":true,"code":4,"v":2}"#,
1635        ] {
1636            assert_eq!(ensure_initial_ack(&parse_ack(json)), Err(Error::BadFrame));
1637        }
1638    }
1639
1640    /// The server's code-4 envelope has no `ok` field; it must still preserve
1641    /// the close code and reason as a typed terminal error.
1642    #[test]
1643    fn the_code_4_error_envelope_surfaces_typed_close_not_a_bad_frame() {
1644        let ack = parse_ack(
1645            r#"{"type":"error","code":4,"reason":"unsupported protocol version; this server speaks wire v2"}"#,
1646        );
1647        assert!(!ack.ok, "an envelope with no `ok` field is not a success");
1648        match ensure_initial_ack(&ack).unwrap_err() {
1649            Error::ApplicationClosed(close) => {
1650                assert_eq!(close.code, 4);
1651                assert_eq!(
1652                    close.reason,
1653                    "unsupported protocol version; this server speaks wire v2"
1654                );
1655                assert_eq!(close.retry_class(), RetryClass::NonRetryable);
1656                assert!(!close.retryable());
1657            }
1658            other => panic!("expected typed code-4 close, got {other:?}"),
1659        }
1660    }
1661
1662    #[test]
1663    fn a_rejection_ack_surfaces_its_reason() {
1664        match ensure_initial_ack(&parse_ack(
1665            r#"{"type":"ack","ok":false,"reason":"quota exceeded: 51 > 50 accounts"}"#,
1666        ))
1667        .unwrap_err()
1668        {
1669            Error::Rejected(reason) => assert_eq!(reason, "quota exceeded: 51 > 50 accounts"),
1670            other => panic!("expected Rejected, got {other:?}"),
1671        }
1672    }
1673
1674    // ---- socket buffer sizing (unrelated to wire v2, unchanged behavior) ---
1675
1676    /// quinn never resizes the socket it is handed, so a client built on
1677    /// `Endpoint::client` silently inherits `net.core.rmem_default` (212992
1678    /// bytes on a stock Linux). Bursts land in a buffer far too small for a
1679    /// shred feed, and the loss shows up as UDP receive errors nobody attributes
1680    /// to us. Assert we actually ask for more.
1681    #[test]
1682    fn client_socket_enlarges_the_receive_buffer() {
1683        let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
1684
1685        let default_size = socket2::Socket::from(std::net::UdpSocket::bind(addr).unwrap())
1686            .recv_buffer_size()
1687            .unwrap();
1688        let ours = socket2::Socket::from(client_socket(addr, DEFAULT_RECV_BUFFER).unwrap())
1689            .recv_buffer_size()
1690            .unwrap();
1691
1692        assert!(
1693            ours > default_size,
1694            "recv buffer {ours} is no larger than the default {default_size}"
1695        );
1696    }
1697
1698    /// The kernel clamps the request to rmem_max rather than failing it, so an
1699    /// unreachably large ask must still yield a usable socket.
1700    #[test]
1701    fn client_socket_survives_a_clamped_request() {
1702        let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
1703
1704        let sock = client_socket(addr, 1 << 30).expect("clamped request must not fail");
1705
1706        assert!(sock.local_addr().is_ok());
1707    }
1708}
1709
1710/// Accepts the server's self-signed certificate without verification.
1711#[derive(Debug)]
1712struct NoVerify;
1713impl rustls::client::danger::ServerCertVerifier for NoVerify {
1714    fn verify_server_cert(
1715        &self,
1716        _e: &rustls::pki_types::CertificateDer<'_>,
1717        _i: &[rustls::pki_types::CertificateDer<'_>],
1718        _s: &rustls::pki_types::ServerName<'_>,
1719        _o: &[u8],
1720        _n: rustls::pki_types::UnixTime,
1721    ) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
1722        Ok(rustls::client::danger::ServerCertVerified::assertion())
1723    }
1724    fn verify_tls12_signature(
1725        &self,
1726        _m: &[u8],
1727        _c: &rustls::pki_types::CertificateDer<'_>,
1728        _d: &rustls::DigitallySignedStruct,
1729    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1730        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
1731    }
1732    fn verify_tls13_signature(
1733        &self,
1734        _m: &[u8],
1735        _c: &rustls::pki_types::CertificateDer<'_>,
1736        _d: &rustls::DigitallySignedStruct,
1737    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1738        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
1739    }
1740    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
1741        rustls::crypto::ring::default_provider()
1742            .signature_verification_algorithms
1743            .supported_schemes()
1744    }
1745}