Skip to main content

sui_protocol/
local.rs

1//! Local protocol — daemon ↔ same-host CLI (and atticd).
2//!
3//! Hot path. Sub-millisecond budget for warm-cache lookups. rkyv 0.8
4//! over a length-prefixed multiplex frame, transported via
5//! `tokio::net::UnixStream`. The transport itself lives in the future
6//! sui-daemon crate; this module ships the typed wire shapes those
7//! transport layers will consume.
8//!
9//! ## Wire shape
10//!
11//! Every byte on the wire belongs to a [`WireFrame`]. The framing is:
12//!
13//! ```text
14//! [magic : 4B = "SUI1"] [length : 4B u32 LE] [body : N bytes of rkyv-archived WireFrame]
15//! ```
16//!
17//! The magic + length prefix is what lets a misaligned reader fail
18//! fast (the rkyv access itself validates the body, so a corrupted
19//! body never gets cast to a typed reference). Length includes only
20//! the body — not the 8 prefix bytes. Max body length is bounded by
21//! daemon configuration; defaults to 64 MiB to comfortably hold a
22//! batched closure-info request without runaway.
23//!
24//! ## Request / response model
25//!
26//! Bidirectional multiplexed: each frame carries a [`RequestId`] (u64)
27//! so multiple in-flight requests share one stream without
28//! head-of-line blocking. Heartbeats are first-class frames so a long
29//! idle connection doesn't NAT-time-out. The daemon may push
30//! unsolicited events (eval-cache invalidations) under `request_id =
31//! 0`, which clients route to a subscription handler.
32
33use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
34
35use sui_graph_store::GraphHash;
36
37/// Magic bytes at the start of every frame on the local protocol.
38/// Bumping requires a `MAX_LOCAL_PROTOCOL_VERSION` bump too.
39pub const FRAME_MAGIC: [u8; 4] = *b"SUI1";
40
41/// Multiplex correlation id. `0` is reserved for unsolicited server
42/// pushes (cache invalidations, progress events).
43pub type RequestId = u64;
44
45/// One frame on the wire. All inter-process communication on the
46/// local link is one or more of these.
47#[derive(
48    Archive,
49    RkyvSerialize,
50    RkyvDeserialize,
51    Debug,
52    Clone,
53    PartialEq,
54)]
55#[rkyv(derive(Debug))]
56pub enum WireFrame {
57    /// Cheap keepalive. Either side sends; receiver echoes the same
58    /// nonce back as another `Heartbeat`. Failure to receive the echo
59    /// within N seconds (caller-configurable) terminates the link.
60    Heartbeat(Heartbeat),
61    /// CLI → daemon request.
62    Request {
63        id: RequestId,
64        body: LocalRequest,
65    },
66    /// Daemon → CLI response (correlates to a prior `Request.id`).
67    Response {
68        id: RequestId,
69        body: LocalResponse,
70    },
71    /// Daemon → CLI event push. `id` is always `0`.
72    Event { topic: String, payload: Vec<u8> },
73    /// Graceful shutdown signal. Either side may send; the receiver
74    /// drains pending responses then closes the connection.
75    Goodbye,
76}
77
78#[derive(
79    Archive,
80    RkyvSerialize,
81    RkyvDeserialize,
82    Debug,
83    Clone,
84    Copy,
85    PartialEq,
86    Eq,
87)]
88#[rkyv(derive(Debug))]
89pub struct Heartbeat {
90    pub nonce: u64,
91    /// Unix nanoseconds (u64). Wraps in year 2554 — well past every
92    /// fleet's useful lifetime.
93    pub sent_unix_nanos: u64,
94}
95
96/// Every request the local protocol can carry today. Append new
97/// variants at the bottom — rkyv tag stability rule.
98#[derive(
99    Archive,
100    RkyvSerialize,
101    RkyvDeserialize,
102    Debug,
103    Clone,
104    PartialEq,
105)]
106#[rkyv(derive(Debug))]
107pub enum LocalRequest {
108    /// Probe — daemon returns build identity + uptime. Used for
109    /// `sui daemon status`.
110    Ping,
111    /// Ask the daemon for the rkyv-archive bytes of a graph stored
112    /// under `(kind_tag, hash)`. The daemon mmaps from
113    /// sui-graph-store and forwards. Zero-copy on the daemon side; one
114    /// memcpy on the CLI side (over the UDS socket).
115    GetGraph {
116        kind_tag: u8,
117        hash: GraphHash,
118    },
119    /// Tell the daemon to ingest a graph — used by tend prebuild and
120    /// any future producer. Daemon validates `BLAKE3(bytes) == hash`
121    /// and persists via sui-graph-store.
122    PutGraph {
123        kind_tag: u8,
124        hash: GraphHash,
125        bytes: Vec<u8>,
126    },
127    /// Ask the daemon for a snapshot of operational counters
128    /// (hot-cache size, hits, misses, last-warm timestamp).
129    /// `sui daemon stats` rides on this.
130    GetStats,
131}
132
133/// Every response the local protocol can carry today.
134#[derive(
135    Archive,
136    RkyvSerialize,
137    RkyvDeserialize,
138    Debug,
139    Clone,
140    PartialEq,
141)]
142#[rkyv(derive(Debug))]
143pub enum LocalResponse {
144    Pong {
145        build_id: [u8; 32],
146        uptime_seconds: u64,
147    },
148    /// Graph bytes (rkyv archive, ready to mmap-cast).
149    GraphBytes(Vec<u8>),
150    /// Confirmation of a successful `PutGraph`.
151    GraphStored {
152        hash: GraphHash,
153    },
154    /// Error returned for any failing request. Carries a typed code
155    /// + free-form message; CLI surfaces both.
156    Error(LocalError),
157    /// Operational counters snapshot.
158    Stats(StatsSnapshot),
159}
160
161/// Daemon-side errors. Codes are stable IDs; messages are operator-
162/// readable strings (English; not localized).
163#[derive(
164    Archive,
165    RkyvSerialize,
166    RkyvDeserialize,
167    Debug,
168    Clone,
169    PartialEq,
170    Eq,
171)]
172#[rkyv(derive(Debug))]
173pub struct LocalError {
174    pub code: ErrorCode,
175    pub message: String,
176}
177
178/// Stable error codes. Append-only.
179#[derive(
180    Archive,
181    RkyvSerialize,
182    RkyvDeserialize,
183    Debug,
184    Clone,
185    Copy,
186    PartialEq,
187    Eq,
188    Hash,
189)]
190#[rkyv(derive(Debug))]
191pub enum ErrorCode {
192    GraphNotFound,
193    GraphHashMismatch,
194    InvalidGraphKind,
195    StoreUnavailable,
196    Internal,
197}
198
199#[derive(
200    Archive,
201    RkyvSerialize,
202    RkyvDeserialize,
203    Debug,
204    Clone,
205    Copy,
206    PartialEq,
207    Eq,
208)]
209#[rkyv(derive(Debug))]
210pub struct StatsSnapshot {
211    pub hot_cache_entries: u64,
212    pub hot_cache_bytes: u64,
213    pub cache_hits: u64,
214    pub cache_misses: u64,
215    pub puts: u64,
216    pub uptime_seconds: u64,
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use pretty_assertions::assert_eq;
223
224    #[test]
225    fn ping_pong_roundtrips() {
226        let frame = WireFrame::Request {
227            id: 42,
228            body: LocalRequest::Ping,
229        };
230        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&frame).unwrap();
231        let archived = rkyv::access::<ArchivedWireFrame, rkyv::rancor::Error>(&bytes).unwrap();
232        // The Archived form mirrors the layout; assert structurally
233        // by matching the variant.
234        match archived {
235            ArchivedWireFrame::Request { id, body } => {
236                assert_eq!(id.to_native(), 42);
237                assert!(matches!(body, ArchivedLocalRequest::Ping));
238            }
239            _ => panic!("expected Request"),
240        }
241    }
242
243    #[test]
244    fn get_graph_request_carries_kind_and_hash() {
245        let h = GraphHash::of(b"sample");
246        let frame = WireFrame::Request {
247            id: 1,
248            body: LocalRequest::GetGraph {
249                kind_tag: 1,
250                hash: h,
251            },
252        };
253        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&frame).unwrap();
254        let archived = rkyv::access::<ArchivedWireFrame, rkyv::rancor::Error>(&bytes).unwrap();
255        match archived {
256            ArchivedWireFrame::Request { body, .. } => match body {
257                ArchivedLocalRequest::GetGraph { kind_tag, hash } => {
258                    assert_eq!(*kind_tag, 1);
259                    assert_eq!(hash.0, h.0);
260                }
261                _ => panic!("expected GetGraph"),
262            },
263            _ => panic!("expected Request"),
264        }
265    }
266
267    #[test]
268    fn error_response_carries_code() {
269        let frame = WireFrame::Response {
270            id: 7,
271            body: LocalResponse::Error(LocalError {
272                code: ErrorCode::GraphNotFound,
273                message: "blob missing".into(),
274            }),
275        };
276        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&frame).unwrap();
277        let archived = rkyv::access::<ArchivedWireFrame, rkyv::rancor::Error>(&bytes).unwrap();
278        match archived {
279            ArchivedWireFrame::Response { body, .. } => match body {
280                ArchivedLocalResponse::Error(err) => {
281                    assert!(matches!(err.code, ArchivedErrorCode::GraphNotFound));
282                    assert_eq!(err.message.as_str(), "blob missing");
283                }
284                _ => panic!("expected Error response"),
285            },
286            _ => panic!("expected Response"),
287        }
288    }
289
290    #[test]
291    fn frame_magic_is_fixed() {
292        assert_eq!(&FRAME_MAGIC, b"SUI1");
293    }
294}