melin_wire_protocol/control.rs
1//! Transport-level control types shared between the server runtime and
2//! the application-specific protocol crate. These are domain-free:
3//! they exist for connection management, auth handshakes, and pipeline
4//! control — not for business messages.
5
6/// Connection identifier assigned by the server.
7///
8/// `u64` — monotonically increasing, never reused within a server
9/// lifetime. Fits in a register and supports more connections than any
10/// single server will ever handle.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct ConnectionId(pub u64);
13
14/// A request's identifier, chosen by the client that sent it.
15///
16/// A transport correlation token: the node echoes it on the request's
17/// final reply frame and keeps no state about it, so it is never checked
18/// for uniqueness or order, and `0` is as valid as any other value. It
19/// means nothing outside its connection's lifetime, and is never
20/// journaled, replicated, or handed to the application.
21///
22/// A newtype so it cannot be swapped with the connection ID it travels
23/// beside. `u64` so a connection that lives for months at millions of
24/// requests a second never has to ask what happens when it wraps.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
26pub struct RequestId(pub u64);
27
28/// The longest refusal reason a `Refused` frame carries, in bytes: its
29/// length goes on the wire as one byte.
30pub const MAX_REFUSAL_REASON: usize = u8::MAX as usize;
31
32/// Why the application's decoder refused a request, as a `Refused` frame
33/// says.
34///
35/// `#[repr(u8)]`, its discriminant the byte on the wire. `0` is no kind,
36/// so a zeroed byte is an error, not a refusal.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[repr(u8)]
39pub enum RefusalKind {
40 /// The key's role may not perform the request.
41 PermissionDenied = 1,
42 /// The request is not well-formed for the application the node runs.
43 Malformed = 2,
44}
45
46impl RefusalKind {
47 /// The kind's byte on the wire.
48 #[inline]
49 pub const fn to_byte(self) -> u8 {
50 self as u8
51 }
52
53 /// The kind a byte names, or `None` for a byte that names none
54 /// (`0` among them). A kind added to the enum is added here too; the
55 /// control codec's tests round-trip every kind and every byte.
56 #[inline]
57 pub const fn from_byte(byte: u8) -> Option<Self> {
58 match byte {
59 1 => Some(Self::PermissionDenied),
60 2 => Some(Self::Malformed),
61 _ => None,
62 }
63 }
64}
65
66/// Transport-level response frames that the server runtime encodes
67/// directly, bypassing the application's `ResponseEncoder` trait.
68///
69/// These are the pipeline-control and auth-handshake messages that
70/// every server sends regardless of the application it hosts.
71///
72/// Every frame that ends a request's reply names the request: the batch
73/// terminators, `ServerBusy` and `Refused` carry its [`RequestId`].
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum TransportResponse {
76 /// Periodic keep-alive sent to idle connections.
77 Heartbeat,
78 /// Marks the end of the response batch for request `id`, backed as
79 /// the ack policy in force requires.
80 BatchEnd { id: RequestId },
81 /// Marks the end of the response batch for request `id` that only
82 /// the primary's own disk backs, a weaker guarantee than the ack
83 /// policy in force. Sent by a primary halted for want of a replica
84 /// for a request it had sequenced before the halt (or a query that
85 /// may reflect such requests), once its own journal holds it. A
86 /// client that needs the policy's guarantee treats the request as
87 /// unconfirmed and reconciles.
88 BatchEndDegraded { id: RequestId },
89 /// Request `id` was shed at ingress: not applied, safe to resend.
90 /// The connection stays open.
91 ServerBusy { id: RequestId },
92 /// The application's decoder refused request `id`: not applied.
93 /// `reason` is the decoder's text, for people; the encoder truncates
94 /// it to [`MAX_REFUSAL_REASON`] bytes on a character boundary.
95 Refused {
96 id: RequestId,
97 kind: RefusalKind,
98 reason: &'static str,
99 },
100 /// Auth handshake: server → client challenge carrying a 32-byte
101 /// random nonce. Encoded with the node's client protocol version in
102 /// front of the nonce.
103 Challenge { nonce: [u8; 32] },
104 /// Auth handshake: server → client rejection.
105 AuthFailed,
106 /// Auth handshake: server → client success.
107 ServerReady,
108}
109
110/// Auth handshake: client → server response to a [`TransportResponse::Challenge`].
111///
112/// Contains the Ed25519 signature over the nonce and the client's
113/// public key for lookup in the authorized-keys table.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct ChallengeResponse {
116 pub signature: [u8; 64],
117 pub public_key: [u8; 32],
118}