Expand description
The link protocol: what two runtimes say to become linked, and what they exchange once
they are. Discovery (super::protocol) proves that a key exists somewhere; a link
proves that the key is answering now, serves the same repository, speaks a
compatible protocol, and is trusted — and only then does anything cooperative
travel.
Two exchanges, both request–answer, both signed end to end:
- Hello → Welcome. The dialer presents its
RuntimeCard(key, runtime, instance, repository, version, features, endpoints), a fresh nonce and its supported protocol range, signed. The answerer verifies the signature, freshness and nonce (a replayed hello is refused), negotiates the protocol, refuses another repository, itself, or an untrusted key — each with a typedRefusalCode— and otherwise answers with its own card, a link id, its marks, and the dialer’s nonce, signed. The dialer verifies all of it and applies the same checks the other way. Mutual: neither side is linked on the other’s word alone. - Sync ↔ Sync. The dialer sends its marks and the events the answerer lacks, under the link id with a rising counter, signed; the answerer ingests, answers with its marks and the events the dialer lacks, signed. One round is one heartbeat and one full replication step in both directions, so a link works even when only one side can reach the other.
Every message is a Signed body: canonical JSON under a per-message domain
separator, so a signature over one message kind can never be replayed as another.
Discovery never grants any of this: a refused hello is recorded, visible, and links
nothing (project.mesh-cooperation-is-authenticated).
use majordomus_cli::mesh::identity::NodeIdentity;
use majordomus_cli::mesh::link::{negotiate, sign, verify_signed, Domain};
let node = NodeIdentity::ephemeral().unwrap();
let signed = sign(&node, Domain::Hello, serde_json::json!({"hello": 1}));
assert!(verify_signed(&node.public.public_key, Domain::Hello, &signed));
assert!(!verify_signed(&node.public.public_key, Domain::Welcome, &signed), "domains do not cross");
assert_eq!(negotiate(1, 3), Some(1));
assert_eq!(negotiate(2, 3), None);Structs§
- Hello
- A dialer’s hello: everything the answerer needs to decide whether to link, in one signed message. The nonce makes the exchange unrepeatable — the welcome must echo it, and a nonce is never accepted twice — and the timestamp makes it unstorable, since a hello captured today cannot be presented tomorrow. The protocol range is stated rather than assumed, so two executables of different ages find their common version here and nowhere else.
- Http
Transport - The transport the shared server answers: plain HTTP/1.1 through the crate’s one minimal client (ADR 0032: no HTTP client dependency, no TLS). A private network or an overlay provides confidentiality; the protocol provides authenticity.
- Link
Refusal - A refusal: the code, the reason in words, and the protocol range of the refuser so a version mismatch is diagnosable from either end.
- Link
Reply - What a hello or a sync answers over the wire: a signed message or a refusal. Both are successful replies — the transport carries them the same way and neither is an error — because a refusal is a decision this protocol wants recorded and shown, and an answer that arrived as an HTTP failure would be indistinguishable from a peer that is down.
- Runtime
Card - What a runtime says about itself on a link: public facts only. The card carries no claim a peer has to take on faith — the node id is derived from the key rather than stated, so a card cannot name a node it cannot sign for, and everything else is either checkable (the repository, the protocol) or advisory (the name, the endpoints).
- Signed
- A signed message: a JSON body and the Ed25519 signature over its canonical bytes under the message’s domain. The body stays JSON so that fields a newer peer adds survive verification here unchanged.
- Sync
Answer - An answerer’s sync answer: the other half of the round. Its marks are taken after ingesting the request’s events, so the dialer learns in one exchange both what arrived and what it is still missing. The report travels with it because a silently dropped event is the failure mode this protocol most needs to make visible: a peer that refuses what it is sent says so, with reasons and numbers, rather than staying quietly behind.
- Sync
Request - A dialer’s sync round: what this runtime holds, and what it believes the answerer lacks. Sending the events rather than asking for them is what makes one heartbeat a full replication step in both directions, so replication keeps working when only one side can open a connection. The counter refuses a replayed round and the instance refuses a stale one: a restarted dialer carries a new instance and must say hello again rather than resume a link the answerer no longer has.
- Welcome
- An answerer’s welcome: the mirror of the hello, plus the two things that make the link usable. The link id is what the dialer syncs under afterwards, and the marks let the very first sync carry exactly the events the answerer lacks instead of a round spent discovering that. The echoed nonce is what binds this welcome to that hello: without it, a welcome kept from an earlier exchange would answer a hello it never saw.
Enums§
- Domain
- The message kinds, each its own signing domain. The domain is mixed into the bytes a signature covers, so a signature made over one kind of message cannot be presented as another: a captured welcome cannot be replayed as a sync answer even though both are signed by the same key.
- Refusal
Code - Why a link message was turned down. Every refusal is typed, counted and shown: a mesh that refuses a peer says which rule did, instead of dropping a socket.
Constants§
- FEATURES
- What this executable’s links can carry. A peer lacking a feature is told so with
RefusalCode::FeatureUnsupportedwhen an operation needs it of that peer. - HELLO_
PATH - The route a hello is posted to.
- LINK_
PROTOCOL_ MAX - The newest link protocol version this executable speaks.
- LINK_
PROTOCOL_ MIN - The oldest link protocol version this executable speaks.
- MAX_
LINK_ MESSAGE - The largest link message, serialized, in bytes: under the HTTP server’s one-megabyte body bound, so the protocol refuses before the transport has to.
- MAX_
LINK_ SKEW_ SECONDS - How far a hello’s or welcome’s timestamp may sit from the reader’s clock.
- REQUEST_
TIMEOUT - One request’s bound.
- SYNC_
EVENT_ BUDGET - The bytes of events one sync message carries at most; the rest follow next round. Never
more than
MAX_LINK_MESSAGEleaves once the rest of the message is serialized: the marks of a journal at its stream bounds weigh hundreds of kilobytes (I2139). - SYNC_
PATH - The route a sync round is posted to.
Traits§
- Link
Transport - How a link message reaches a peer. The domain above never names a socket: the HTTP adapter is the one the shared server answers, and a test can put two runtimes in one process behind another adapter without changing a line of protocol.
Functions§
- fresh_
token - 32 hex characters of OS entropy: nonces and link ids. Both uses need unpredictability rather than uniqueness alone — a guessable nonce would let a captured hello be answered in advance, and a guessable link id would let a stranger sync under somebody else’s link. When the OS has no entropy to give, a clock-derived value keeps the link working and the replay caches still refuse exact repeats.
- negotiate
- The version both sides speak: the highest in the intersection of their ranges, or
Nonewhen the ranges do not meet. - sign
- Sign
bodyas a message ofdomain. The signature is taken over the domain separator followed by the canonical JSON of the body, so two peers that serialize the same facts in different key orders still agree about what was signed, and nothing signed for one exchange verifies as part of another. - verify_
signed - Whether
signedis a message ofdomainsigned bypublic_key. Three things have to hold at once — the right key, the right domain, and an untouched body — and this answers about all three with one boolean, because a link has no use for the difference: any of them failing means the message does not come from the peer it claims to.