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}