Skip to main content

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}