cultnet-rs: IdunnRuntimePresencePublisher, Odin RUDP delivery of signed presence and the Warming lease wait

Endpoint from Expected's one odin.verse-rendezvous shared-infrastructure dependency; one RUDP session per publish; ack is delivery, an Odin error frame is a refusal by code; message id from the record's sequence; the signer is borrowed per call. Seam tests run against cultmesh-rs's real document server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-29 18:27:12 +01:00
commit 4136167b41
5 changed files with 1088 additions and 9 deletions

View file

@ -37,6 +37,7 @@ libc = "0.2"
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security_Cryptography"] }
[dev-dependencies]
cultmesh-rs = { path = "../cultmesh-rs" }
pretty_assertions = "1.4"
tempfile = "3"

View file

@ -4,8 +4,10 @@
//! runtime bundle with the two signing credentials Idunn hands it, reports its
//! health as it changes, and signs presence records that Odin can authenticate
//! and Idunn's route challenge can read. This module owns that provider half and
//! owns no network; publishing the signed records is a separate concern that
//! consumes [`IdunnRuntimePresenceSigner::sign`].
//! owns no network; [`IdunnRuntimePresencePublisher`] delivers the signed records
//! to Odin and waits for Idunn's write lease while Warming. A target whose
//! Expected declares no Odin dependency is route-proof: `for_signer` returns
//! `None` and `answer_route_observation` is its whole presence.
//!
//! The module compiles and runs on every platform an Idunn driver launches:
//! Idunn's systemd driver (unix) delivers the credentials as inherited
@ -14,9 +16,11 @@
//! lock are unix-only.
//!
//! ```no_run
//! use std::time::Duration;
//!
//! use cultnet_rs::{
//! GameCultRuntimeCapability, IdunnRuntimeAuthority, IdunnRuntimeCredentials,
//! IdunnRuntimePresenceSigner, RuntimePresenceState,
//! IdunnRuntimePresencePublisher, IdunnRuntimePresenceSigner, RuntimePresenceState,
//! };
//!
//! # fn main() -> anyhow::Result<()> {
@ -40,10 +44,13 @@
//! capacity: 1,
//! }],
//! );
//! let warming = signer.sign("starting")?; // the signer starts Warming
//! signer.report_health(RuntimePresenceState::Active);
//! let active = signer.sign("ready")?;
//! # let _ = (warming, active);
//! // Odin's endpoint comes from Expected; the publisher borrows the signer per call.
//! if let Some(publisher) = IdunnRuntimePresencePublisher::for_signer(&signer)? {
//! // The signer starts Warming; heartbeats until Idunn writes the lease.
//! let _lease = publisher.wait_for_write_lease(&mut signer, Duration::from_secs(60))?;
//! signer.report_health(RuntimePresenceState::Active);
//! publisher.publish(&mut signer, "ready")?;
//! }
//! # Ok(())
//! # }
//! ```
@ -1400,5 +1407,8 @@ fn unix_millis() -> Result<u64> {
.context("Unix time exceeds u64")
}
mod odin_publisher;
pub use odin_publisher::IdunnRuntimePresencePublisher;
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,424 @@
//! Delivery of signed runtime presence to Odin's document catalog.
//!
//! [`IdunnRuntimePresencePublisher`] is the network half of the Idunn provider.
//! It owns where signed presence goes and how it gets there; the signer it
//! borrows on every call stays the one owner of health, sequence, Warming proofs
//! and the write-lease guard. The publisher keeps no state of its own: Odin's
//! endpoint is read once from the one `shared-infrastructure`
//! `odin.verse-rendezvous` dependency in Expected, and each publish is one
//! fresh RUDP session, so an Odin restart needs no reconnect machinery.
//!
//! Blocking `std::net` only. It builds and runs wherever the provider does.
use std::net::{ToSocketAddrs, UdpSocket};
use std::time::{Duration, Instant};
use anyhow::{Result, bail, ensure};
use super::{
IdunnRuntimePresenceSigner, ProcessWriteLeaseGuard, RuntimePresenceState,
SignedRuntimePresence,
};
use crate::{
CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID, CultNetErrorCode, CultNetMessage,
CultNetRudpReliableSendStatus, CultNetRudpSocketTransportConnection,
CultNetRudpSocketTransportOptions, CultNetWireContract, encode_cultnet_message_to_vec,
};
const ODIN_DEPENDENCY_KIND: &str = "shared-infrastructure";
const ODIN_CAPABILITY: &str = "odin.verse-rendezvous";
/// Names the field in refusals; never the value.
const ENDPOINT_FIELD: &str = "Expected odin.verse-rendezvous dependency provider_endpoint";
const RUDP_SCHEME: &str = "rudp://";
/// How long one socket read waits, so the delivery loops poll without spinning.
const RECEIVE_SLICE: Duration = Duration::from_millis(25);
const RESEND_DELAY_MS: u64 = 100;
/// The publisher's timing. Crate-private: tests shorten it, consumers do not
/// tune it. A fixed 5 s heartbeat under Idunn's own Warming deadline needs no
/// backoff.
#[derive(Clone, Copy, Debug)]
pub(super) struct Cadence {
pub(super) heartbeat: Duration,
pub(super) lease_poll: Duration,
pub(super) connect: Duration,
pub(super) ack: Duration,
}
impl Cadence {
const PRODUCTION: Self = Self {
heartbeat: Duration::from_secs(5),
lease_poll: Duration::from_millis(100),
connect: Duration::from_secs(2),
ack: Duration::from_secs(2),
};
}
/// Why one delivery failed. Each variant is a fixed code: nothing here carries
/// Odin's text, an address or a path.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DeliveryFailure {
Unresolved,
NoSocket,
ConnectTimeout,
SendFailed,
AckTimeout,
SendInvalidated,
ReplyUnreadable,
SessionEnded,
Refused(Option<CultNetErrorCode>),
}
impl std::fmt::Display for DeliveryFailure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("Odin delivery failed: ")?;
match self {
Self::Unresolved => formatter.write_str("endpoint_unresolved"),
Self::NoSocket => formatter.write_str("socket_unavailable"),
Self::ConnectTimeout => formatter.write_str("connect_timeout"),
Self::SendFailed => formatter.write_str("send_failed"),
Self::AckTimeout => formatter.write_str("ack_timeout"),
Self::SendInvalidated => formatter.write_str("send_invalidated"),
Self::ReplyUnreadable => formatter.write_str("reply_unreadable"),
Self::SessionEnded => formatter.write_str("session_ended"),
Self::Refused(Some(code)) => write!(formatter, "odin_refused code {code:?}"),
Self::Refused(None) => formatter.write_str("odin_refused without a code"),
}
}
}
impl std::error::Error for DeliveryFailure {}
/// Delivers an Idunn-managed runtime's signed presence to Odin and waits for
/// Idunn's write lease while Warming.
///
/// Built from a signer, it borrows that signer on every call, so route answers
/// and publishes share one signer and one sequence. Health is whatever the
/// signer was last told with
/// [`report_health`](IdunnRuntimePresenceSigner::report_health); the publisher
/// has no state parameter.
pub struct IdunnRuntimePresencePublisher {
host: String,
port: u16,
expected_sha256: String,
runtime_instance_id: String,
cadence: Cadence,
}
impl std::fmt::Debug for IdunnRuntimePresencePublisher {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("IdunnRuntimePresencePublisher")
.finish_non_exhaustive()
}
}
impl IdunnRuntimePresencePublisher {
/// The publisher for `signer`'s Odin, or `None` when Expected declares no
/// `shared-infrastructure` `odin.verse-rendezvous` dependency. Such a
/// target is route-proof: Idunn reads its presence through the route
/// challenge, which [`answer_route_observation`](IdunnRuntimePresenceSigner::answer_route_observation)
/// answers with no Odin.
///
/// Refuses two such dependencies, a missing `provider_endpoint`, a scheme
/// other than `rudp://` and an endpoint that is not `host:port`. A refusal
/// names the field, never the value. The host is resolved at each publish.
pub fn for_signer(signer: &IdunnRuntimePresenceSigner) -> Result<Option<Self>> {
let records = &signer.authority.records;
let mut odin = records.expected.dependencies.iter().filter(|dependency| {
dependency.kind == ODIN_DEPENDENCY_KIND && dependency.capability == ODIN_CAPABILITY
});
let Some(dependency) = odin.next() else {
return Ok(None);
};
ensure!(
odin.next().is_none(),
"Expected declares more than one odin.verse-rendezvous shared-infrastructure dependency"
);
let Some(endpoint) = dependency.provider_endpoint.as_deref() else {
bail!("{ENDPOINT_FIELD} is missing");
};
let Some((host, port)) = parse_endpoint(endpoint) else {
bail!("{ENDPOINT_FIELD} must be rudp://host:port or host:port");
};
Ok(Some(Self {
host,
port,
expected_sha256: records.expected_sha256.clone(),
runtime_instance_id: records.activation.runtime_instance_id.clone(),
cadence: Cadence::PRODUCTION,
}))
}
#[cfg(test)]
pub(super) fn with_cadence(mut self, cadence: Cadence) -> Self {
self.cadence = cadence;
self
}
/// Signs one presence in the signer's reported health and delivers it. It
/// returns after Odin acknowledged the document. The ack proves delivery;
/// Odin's correlation owns admission. A CultNet error frame from Odin is a
/// refusal naming its code, never its text.
pub fn publish(
&self,
signer: &mut IdunnRuntimePresenceSigner,
detail: &str,
) -> Result<SignedRuntimePresence> {
let (signed, delivery) = self.sign_and_deliver(signer, detail)?;
delivery?;
Ok(signed)
}
/// Waits for Idunn's process write lease, publishing a Warming heartbeat on
/// entry and every 5 s and polling for the lease every 100 ms. The signer
/// must report Warming; the wait is refused before any publish otherwise.
/// Returns `Ok(None)` at once when Expected requires no lease.
///
/// The caller sets `timeout` because Idunn owns the Warming deadline. A
/// heartbeat that fails delivery is retried at the next heartbeat and does
/// not end the wait: the signer remembers its Warming proofs whether or not
/// delivery succeeded. A lease record that binds another incarnation ends
/// the wait at once. A timeout names the count of failed deliveries and the
/// last one's code.
pub fn wait_for_write_lease(
&self,
signer: &mut IdunnRuntimePresenceSigner,
timeout: Duration,
) -> Result<Option<ProcessWriteLeaseGuard>> {
self.require_own_signer(signer)?;
if !signer.authority.records.expected.write_lease_required {
return Ok(None);
}
ensure!(
signer.health() == RuntimePresenceState::Warming,
"waiting for the write lease requires the signer to report Warming"
);
let started = Instant::now();
let mut next_heartbeat = started;
let mut failed_deliveries = 0_u32;
let mut last_failure = None;
loop {
let now = Instant::now();
if now >= next_heartbeat {
let (_, delivery) = self.sign_and_deliver(signer, "waiting-for-process-write-lease")?;
if let Err(failure) = delivery {
failed_deliveries += 1;
last_failure = Some(failure);
}
next_heartbeat = now + self.cadence.heartbeat;
}
if let Some(guard) = signer.try_acquire_write_lease()? {
return Ok(Some(guard));
}
let Some(remaining) = timeout.checked_sub(started.elapsed()) else {
bail!(
"timed out waiting for Idunn's process write lease: {failed_deliveries} failed deliveries{}",
last_failure.map_or(String::new(), |failure| format!(", last: {failure}"))
);
};
std::thread::sleep(self.cadence.lease_poll.min(remaining));
}
}
/// Every presence that reaches Odin goes through here. The outer error is a
/// refusal before delivery; the inner result is the delivery.
fn sign_and_deliver(
&self,
signer: &mut IdunnRuntimePresenceSigner,
detail: &str,
) -> Result<(SignedRuntimePresence, Result<(), DeliveryFailure>)> {
self.require_own_signer(signer)?;
let signed = signer.sign(detail)?;
let delivery = self.deliver(&signed);
Ok((signed, delivery))
}
fn require_own_signer(&self, signer: &IdunnRuntimePresenceSigner) -> Result<()> {
let records = &signer.authority.records;
ensure!(
records.expected_sha256 == self.expected_sha256
&& records.activation.runtime_instance_id == self.runtime_instance_id,
"this publisher was built for another runtime authority"
);
Ok(())
}
/// One fresh session: connect, one reliable `DocumentPutRaw` on `schema`,
/// wait for the ack. The message id comes from the signed record, so the
/// publisher keeps no counter.
fn deliver(&self, signed: &SignedRuntimePresence) -> Result<(), DeliveryFailure> {
let remote = (self.host.as_str(), self.port)
.to_socket_addrs()
.ok()
.and_then(|mut addresses| addresses.next())
.ok_or(DeliveryFailure::Unresolved)?;
let socket = UdpSocket::bind(if remote.is_ipv4() {
"0.0.0.0:0"
} else {
"[::]:0"
})
.map_err(|_| DeliveryFailure::NoSocket)?;
socket
.set_read_timeout(Some(RECEIVE_SLICE))
.map_err(|_| DeliveryFailure::NoSocket)?;
let mut options = CultNetRudpSocketTransportOptions::client(
signed.record.runtime_id.clone(),
socket,
remote,
CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID,
);
options.resend_delay_ms = RESEND_DELAY_MS;
let mut transport = CultNetRudpSocketTransportConnection::new(options)
.map_err(|_| DeliveryFailure::NoSocket)?;
transport
.connect(Vec::new())
.map_err(|_| DeliveryFailure::SendFailed)?;
let connect_deadline = Instant::now() + self.cadence.connect;
loop {
pump(&mut transport)?;
if transport.connected() {
break;
}
if Instant::now() >= connect_deadline {
return Err(DeliveryFailure::ConnectTimeout);
}
}
let message = CultNetMessage::DocumentPutRaw {
message_id: format!(
"runtime-presence:{}:{}:{}",
signed.record.target,
signed.record.runtime_instance_id,
signed.record.publisher_sequence
),
document: signed.document.clone(),
};
let payload = encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)
.map_err(|_| DeliveryFailure::SendFailed)?;
let receipt = transport
.send_reliable("schema", payload)
.map_err(|_| DeliveryFailure::SendFailed)?;
let ack_deadline = Instant::now() + self.cadence.ack;
loop {
pump(&mut transport)?;
match transport.reliable_send_status(&receipt) {
CultNetRudpReliableSendStatus::Acknowledged => break,
CultNetRudpReliableSendStatus::Invalidated => {
return Err(DeliveryFailure::SendInvalidated);
}
CultNetRudpReliableSendStatus::Pending => {}
}
if Instant::now() >= ack_deadline {
return Err(DeliveryFailure::AckTimeout);
}
}
// Best effort: the session is discarded either way.
let _ = transport.disconnect(Vec::new());
Ok(())
}
}
/// Reads what Odin has sent, resends what is due and reports a refusal or an
/// ended session. Waits at most one socket read slice.
fn pump(transport: &mut CultNetRudpSocketTransportConnection) -> Result<(), DeliveryFailure> {
loop {
match transport.receive_schema_message_once() {
Ok(Some(CultNetMessage::Error { code, .. })) => {
return Err(DeliveryFailure::Refused(code));
}
Ok(Some(_)) => {}
Ok(None) => break,
Err(_) => return Err(DeliveryFailure::ReplyUnreadable),
}
}
if transport.disconnect_reason().is_some() {
return Err(DeliveryFailure::SessionEnded);
}
transport
.poll_resends()
.map_err(|_| DeliveryFailure::SendFailed)
}
/// `rudp://host:port` or bare `host:port` (an IPv6 host in brackets) as host
/// and port. Any other scheme, a path, a blank host or a zero port is `None`.
fn parse_endpoint(endpoint: &str) -> Option<(String, u16)> {
let rest = match endpoint.strip_prefix(RUDP_SCHEME) {
Some(rest) => rest,
None if endpoint.contains("://") => return None,
None => endpoint,
};
let (host, port) = if let Some(bracketed) = rest.strip_prefix('[') {
let (host, after) = bracketed.split_once(']')?;
(host, after.strip_prefix(':')?)
} else {
let (host, port) = rest.rsplit_once(':')?;
if host.contains(':') {
return None;
}
(host, port)
};
if host.is_empty() || host.contains(|c: char| c.is_whitespace() || c == '/') {
return None;
}
let port: u16 = port.parse().ok().filter(|port| *port != 0)?;
Some((host.to_owned(), port))
}
#[cfg(test)]
mod tests {
use super::*;
fn parsed(endpoint: &str) -> Option<(&'static str, u16)> {
parse_endpoint(endpoint).map(|(host, port)| (Box::leak(host.into_boxed_str()) as &str, port))
}
#[test]
fn endpoints_are_rudp_or_bare_host_and_port() {
assert_eq!(parsed("rudp://odin.example:7001"), Some(("odin.example", 7001)));
assert_eq!(parsed("10.0.0.5:7001"), Some(("10.0.0.5", 7001)));
assert_eq!(parsed("rudp://[::1]:7001"), Some(("::1", 7001)));
assert_eq!(parsed("[fd00::5]:65535"), Some(("fd00::5", 65535)));
for bad in [
"",
"odin.example",
"odin.example:0",
"odin.example:65536",
"odin.example:port",
":7001",
"udp://odin.example:7001",
"http://odin.example:7001",
"rudp://odin.example:7001/x",
"odin example:7001",
"::1:7001",
"[::1]",
"[::1]7001",
"rudp://rudp://odin.example:7001",
] {
assert_eq!(parse_endpoint(bad), None, "{bad:?}");
}
}
#[test]
fn production_cadence_is_the_ruled_one() {
let cadence = Cadence::PRODUCTION;
assert_eq!(cadence.heartbeat, Duration::from_secs(5));
assert_eq!(cadence.lease_poll, Duration::from_millis(100));
assert_eq!(cadence.connect, Duration::from_secs(2));
assert_eq!(cadence.ack, Duration::from_secs(2));
}
/// Odin's server sends error frames without a code, so a coded refusal is
/// reachable only here.
#[test]
fn a_refusal_names_its_code_and_nothing_else() {
let coded = DeliveryFailure::Refused(Some(CultNetErrorCode::WriteNotAuthorized));
assert_eq!(
coded.to_string(),
"Odin delivery failed: odin_refused code WriteNotAuthorized"
);
assert_eq!(
DeliveryFailure::Refused(None).to_string(),
"Odin delivery failed: odin_refused without a code"
);
}
}

View file

@ -5,7 +5,7 @@ use cultcache_rs::{CacheBackingStore, CultCacheEnvelope};
use super::*;
use crate::{
IdunnExpectedCapability, IdunnExpectedRoute, IdunnRuntimeActivationLaunch,
IdunnExpectedCapability, IdunnExpectedDependency, IdunnExpectedRoute, IdunnRuntimeActivationLaunch,
IdunnServiceIdentity, RUNTIME_PRESENCE_IDENTITY_FD_NAME, RuntimePresenceAuthenticationContext,
VerifiedRuntimeAuthority, authenticate_runtime_presence_claim,
correlate_runtime_presence_claim, enroll_service_identity_at, verify_runtime_authority,
@ -114,6 +114,13 @@ struct Rig {
impl Rig {
fn new(write_lease_required: bool) -> Result<Self> {
Self::with_dependencies(write_lease_required, Vec::new())
}
fn with_dependencies(
write_lease_required: bool,
dependencies: Vec<IdunnExpectedDependency>,
) -> Result<Self> {
let dir = tempfile::tempdir()?;
let provider_path = dir.path().join("provider.cc");
let provider =
@ -152,7 +159,7 @@ impl Rig {
minimum_capacity: 1,
})
.collect(),
dependencies: Vec::new(),
dependencies,
};
let (activation, activation_seed) = Self::issue(&expected, &idunn)?;
let rig = Self {
@ -2118,3 +2125,5 @@ fn host_credentials_keep_the_seed_when_the_provider_is_a_directory() -> Result<(
assert!(seed.exists(), "a refused delivery leaves the one-shot seed");
Ok(())
}
mod odin_publisher;

View file

@ -0,0 +1,635 @@
//! The publisher against cultmesh-rs's real RUDP document server, the receiver
//! Odin runs. cultmesh-rs links the non-test build of this crate, so the
//! server's types are a second instance of ours: only bytes and strings cross.
use std::net::{SocketAddr, UdpSocket};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use cultmesh_rs::{
CultMeshRudpDocumentServer, CultMeshRudpDocumentServerOptions, CultMeshRudpPollOutcome,
CultMeshRudpRawDocumentReceipt, CultMeshRudpSnapshotQuery, CultMeshSystemClock,
};
use super::*;
use crate::idunn_runtime_provider::odin_publisher::Cadence;
const ODIN_CAPABILITY: &str = "odin.verse-rendezvous";
/// Text a refusing sink carries; Odin's error frame must not repeat it.
const SINK_CANARY: &str = "SINK-CANARY-8841";
const TEST_CADENCE: Cadence = Cadence {
heartbeat: Duration::from_millis(100),
lease_poll: Duration::from_millis(5),
connect: Duration::from_millis(500),
ack: Duration::from_millis(500),
};
// --------------------------------------------------------------- Odin harness
#[derive(Clone, Debug)]
struct Delivered {
message_id: String,
record_key: String,
schema_id: String,
payload: Vec<u8>,
from: SocketAddr,
}
impl Delivered {
fn record(&self) -> GameCultRuntimePresenceHealthRecord {
rmp_serde::from_slice(&self.payload).expect("a delivered presence record decodes")
}
}
#[derive(Clone, Copy)]
enum Sink {
Accept,
Refuse,
}
/// cultmesh-rs's document server on a thread, with a sink that records or refuses.
struct Odin {
addr: SocketAddr,
received: Arc<Mutex<Vec<Delivered>>>,
stop: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
}
impl Odin {
fn start(sink: Sink) -> Self {
Self::start_on("127.0.0.1:0".parse().unwrap(), sink)
}
fn start_on(addr: SocketAddr, sink: Sink) -> Self {
let socket = UdpSocket::bind(addr).expect("the Odin test socket binds");
let addr = socket.local_addr().unwrap();
let received = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let record = Arc::clone(&received);
let mut server = CultMeshRudpDocumentServer::new(
socket,
move |receipt: CultMeshRudpRawDocumentReceipt| -> Result<()> {
match sink {
Sink::Refuse => bail!(SINK_CANARY),
Sink::Accept => {
record.lock().unwrap().push(Delivered {
message_id: receipt.message_id,
record_key: receipt.document.record_key,
schema_id: receipt.document.schema_id,
payload: receipt.document.payload,
from: receipt.session.remote_addr,
});
Ok(())
}
}
},
|_: &CultMeshRudpSnapshotQuery| -> Result<Vec<_>> { Ok(Vec::new()) },
CultMeshSystemClock::default(),
CultMeshRudpDocumentServerOptions::default(),
)
.expect("the Odin test server starts");
let running = Arc::clone(&stop);
let thread = std::thread::spawn(move || {
while !running.load(Ordering::Relaxed) {
if matches!(server.poll_once(), Ok(CultMeshRudpPollOutcome::Idle) | Err(_)) {
std::thread::sleep(Duration::from_millis(1));
}
}
});
Self {
addr,
received,
stop,
thread: Some(thread),
}
}
fn deliveries(&self) -> Vec<Delivered> {
self.received.lock().unwrap().clone()
}
fn count(&self) -> usize {
self.received.lock().unwrap().len()
}
fn wait_for(&self, count: usize) -> Vec<Delivered> {
let give_up = Instant::now() + Duration::from_secs(10);
while self.count() < count {
assert!(Instant::now() < give_up, "Odin never received {count}");
std::thread::sleep(Duration::from_millis(2));
}
self.deliveries()
}
fn rudp_endpoint(&self) -> String {
format!("rudp://{}", self.addr)
}
}
impl Drop for Odin {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
/// An address nothing listens on.
fn dead_address() -> SocketAddr {
UdpSocket::bind("127.0.0.1:0").unwrap().local_addr().unwrap()
}
// ----------------------------------------------------------------------- rigs
fn odin_dependency(schema: &str, endpoint: Option<String>) -> IdunnExpectedDependency {
IdunnExpectedDependency {
kind: "shared-infrastructure".into(),
capability: ODIN_CAPABILITY.into(),
schema: schema.into(),
compatibility: "v1".into(),
minimum_capacity: 1,
startup: "before-start".into(),
provider_id: Some("odin-ygg-1".into()),
provider_authority: Some("managed-incarnation".into()),
provider_expected_projection_sha256: Some(digest('9')),
provider_endpoint: endpoint,
}
}
fn rig_with(
write_lease_required: bool,
dependencies: Vec<IdunnExpectedDependency>,
) -> Result<Rig> {
Rig::with_dependencies(write_lease_required, dependencies)
}
fn odin_rig(write_lease_required: bool, endpoint: &str) -> Result<Rig> {
rig_with(
write_lease_required,
vec![odin_dependency("odin.verse.v1", Some(endpoint.into()))],
)
}
fn publisher_for(
signer: &IdunnRuntimePresenceSigner,
cadence: Cadence,
) -> Result<IdunnRuntimePresencePublisher> {
Ok(IdunnRuntimePresencePublisher::for_signer(signer)?
.expect("Expected declares an Odin dependency")
.with_cadence(cadence))
}
// ------------------------------------------------------------------- endpoint
#[test]
fn for_signer_reads_endpoint_from_expected() -> Result<()> {
let odin = Odin::start(Sink::Accept);
// `rudp://host:port` and bare `host:port` both reach the dependency's server.
for endpoint in [odin.rudp_endpoint(), odin.addr.to_string()] {
let rig = odin_rig(false, &endpoint)?;
let mut signer = rig.signer()?;
let publisher = publisher_for(&signer, TEST_CADENCE)?;
publisher.publish(&mut signer, "hello")?;
}
assert_eq!(odin.count(), 2);
// No Odin dependency: the target is route-proof and has no publisher.
let rig = Rig::new(false)?;
assert!(IdunnRuntimePresencePublisher::for_signer(&rig.signer()?)?.is_none());
// Only a shared-infrastructure dependency on Odin is Odin.
let mut bootstrap = odin_dependency("odin.verse.v1", Some(odin.rudp_endpoint()));
bootstrap.kind = "bootstrap".into();
let mut other = odin_dependency("odin.verse.v1", Some(odin.rudp_endpoint()));
other.capability = "telemetry".into();
let rig = rig_with(false, vec![bootstrap])?;
assert!(IdunnRuntimePresencePublisher::for_signer(&rig.signer()?)?.is_none());
let rig = rig_with(false, vec![other])?;
assert!(IdunnRuntimePresencePublisher::for_signer(&rig.signer()?)?.is_none());
// IPv6 hosts are bracketed.
let rig = odin_rig(false, "rudp://[::1]:9000")?;
assert!(IdunnRuntimePresencePublisher::for_signer(&rig.signer()?)?.is_some());
Ok(())
}
#[test]
fn for_signer_refuses_what_is_not_one_usable_endpoint() -> Result<()> {
let canary = "canary-host.invalid";
let two = rig_with(
false,
vec![
odin_dependency("odin.verse.v1", Some(format!("{canary}:9000"))),
odin_dependency("odin.verse.v2", Some(format!("{canary}:9001"))),
],
)?;
let refused = refusal(IdunnRuntimePresencePublisher::for_signer(&two.signer()?));
assert!(refused.contains(ODIN_CAPABILITY), "{refused}");
assert!(!refused.contains(canary), "{refused}");
let missing = rig_with(false, vec![odin_dependency("odin.verse.v1", None)])?;
let refused = refusal(IdunnRuntimePresencePublisher::for_signer(&missing.signer()?));
assert!(refused.contains("provider_endpoint"), "{refused}");
for bad in [
format!("udp://{canary}:9000"),
format!("http://{canary}:9000"),
format!("tcp://{canary}:9000"),
canary.to_string(),
format!("{canary}:0"),
format!("{canary}:notaport"),
format!("{canary}:9000/path"),
format!(":{}", 9000),
] {
let rig = rig_with(false, vec![odin_dependency("odin.verse.v1", Some(bad.clone()))])?;
let refused = refusal(IdunnRuntimePresencePublisher::for_signer(&rig.signer()?));
assert!(
refused.contains("provider_endpoint"),
"{bad} was not refused by field: {refused}"
);
assert!(!refused.contains(canary), "{refused}");
}
Ok(())
}
// -------------------------------------------------------------------- publish
#[test]
fn publish_delivers_to_odin_catalog_server() -> Result<()> {
let odin = Odin::start(Sink::Accept);
let rig = odin_rig(false, &odin.rudp_endpoint())?;
let mut signer = rig.signer()?;
let publisher = publisher_for(&signer, TEST_CADENCE)?;
let signed = publisher.publish(&mut signer, "hello")?;
// The sink ran before the ack, so the document is already there.
let delivered = odin.deliveries();
assert_eq!(delivered.len(), 1);
let delivered = &delivered[0];
assert_eq!(delivered.payload, signed.document.payload);
assert_eq!(delivered.record_key, TARGET);
assert_eq!(delivered.schema_id, GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA);
assert_eq!(
delivered.message_id,
format!(
"runtime-presence:{TARGET}:{}:{}",
signed.record.runtime_instance_id, signed.record.publisher_sequence
)
);
assert_eq!(signed.record.publisher_sequence, 1);
assert_odin_accepts(&rig, &signed, signed.record.observed_at_unix_millis + 1)?;
Ok(())
}
#[test]
fn publish_surfaces_odin_refusal() -> Result<()> {
let odin = Odin::start(Sink::Refuse);
let rig = odin_rig(false, &odin.rudp_endpoint())?;
let mut signer = rig.signer()?;
let publisher = publisher_for(&signer, TEST_CADENCE)?;
let refused = refusal(publisher.publish(&mut signer, "hello"));
assert!(refused.contains("odin_refused"), "{refused}");
assert!(!refused.contains(SINK_CANARY), "{refused}");
assert!(!refused.contains("127.0.0.1"), "{refused}");
assert!(!refused.contains(&odin.addr.port().to_string()), "{refused}");
Ok(())
}
#[test]
fn publish_times_out_without_echoing_endpoint() -> Result<()> {
let dead = dead_address();
let rig = odin_rig(false, &format!("rudp://{dead}"))?;
let mut signer = rig.signer()?;
let cadence = Cadence {
connect: Duration::from_millis(300),
..TEST_CADENCE
};
let publisher = publisher_for(&signer, cadence)?;
let started = Instant::now();
let refused = refusal(publisher.publish(&mut signer, "hello"));
let elapsed = started.elapsed();
assert!(refused.contains("connect_timeout"), "{refused}");
assert!(!refused.contains("127.0.0.1"), "{refused}");
assert!(!refused.contains(&dead.port().to_string()), "{refused}");
assert!(elapsed >= Duration::from_millis(300), "{elapsed:?}");
assert!(elapsed < Duration::from_millis(1_500), "{elapsed:?}");
Ok(())
}
#[test]
fn publisher_states_signer_health() -> Result<()> {
let odin = Odin::start(Sink::Accept);
let rig = odin_rig(false, &odin.rudp_endpoint())?;
let mut signer = rig.signer()?;
let publisher = publisher_for(&signer, TEST_CADENCE)?;
publisher.publish(&mut signer, "first")?;
signer.report_health(RuntimePresenceState::Degraded);
let signed = publisher.publish(&mut signer, "second")?;
let delivered = odin.deliveries();
assert_eq!(delivered[0].record().state, "warming");
assert_eq!(delivered[1].record().state, "degraded");
assert_eq!(signed.record.state, "degraded");
Ok(())
}
#[test]
fn two_publishes_strictly_increase_sequence_in_fresh_sessions() -> Result<()> {
let odin = Odin::start(Sink::Accept);
let rig = odin_rig(false, &odin.rudp_endpoint())?;
let mut signer = rig.signer()?;
let publisher = publisher_for(&signer, TEST_CADENCE)?;
publisher.publish(&mut signer, "one")?;
// A route answer signs from the same signer, so it takes sequence 2.
let answer = signer.answer_route_observation(&CultNetMessage::SnapshotRequest {
message_id: "challenge".into(),
schema_ids: None,
record_keys: None,
})?;
assert!(matches!(answer, CultNetMessage::SnapshotResponseRaw { .. }));
publisher.publish(&mut signer, "two")?;
let delivered = odin.deliveries();
assert_eq!(delivered.len(), 2);
assert_eq!(delivered[0].record().publisher_sequence, 1);
assert_eq!(delivered[1].record().publisher_sequence, 3);
assert!(delivered[0].message_id.ends_with(":1"), "{:?}", delivered[0]);
assert!(delivered[1].message_id.ends_with(":3"), "{:?}", delivered[1]);
assert_ne!(
delivered[0].from, delivered[1].from,
"each publish is its own RUDP session"
);
Ok(())
}
#[test]
fn publisher_refuses_foreign_signer() -> Result<()> {
let odin = Odin::start(Sink::Accept);
let rig = odin_rig(false, &odin.rudp_endpoint())?;
let other = odin_rig(false, &odin.rudp_endpoint())?;
let publisher = publisher_for(&rig.signer()?, TEST_CADENCE)?;
let mut foreign = other.signer()?;
let refused = refusal(publisher.publish(&mut foreign, "hello"));
assert!(refused.contains("another runtime authority"), "{refused}");
let refused = refusal(publisher.wait_for_write_lease(&mut foreign, Duration::from_secs(1)));
assert!(refused.contains("another runtime authority"), "{refused}");
assert_eq!(odin.count(), 0, "nothing was delivered");
assert_eq!(
foreign.sign("untouched")?.record.publisher_sequence,
1,
"a refused signer signed nothing"
);
Ok(())
}
// ----------------------------------------------------------------- lease wait
#[test]
fn lease_wait_not_required_returns_none_without_publishing() -> Result<()> {
let odin = Odin::start(Sink::Accept);
let rig = odin_rig(false, &odin.rudp_endpoint())?;
let mut signer = rig.signer()?;
let publisher = publisher_for(&signer, TEST_CADENCE)?;
let guard = publisher.wait_for_write_lease(&mut signer, Duration::from_secs(5))?;
assert!(guard.is_none());
assert_eq!(odin.count(), 0);
assert_eq!(signer.sign("untouched")?.record.publisher_sequence, 1);
Ok(())
}
#[test]
fn catalog_connection_id_has_one_spelling() {
assert_eq!(
cultmesh_rs::CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID,
crate::CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID
);
assert_eq!(
crate::CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID,
0x0d1d_0002,
"Odin's catalog id is wire contract"
);
}
/// The lease holder needs the lock files of a unix host; Windows refuses a
/// lease requirement when it opens the authority.
#[cfg(unix)]
mod lease_wait {
use super::*;
/// Writes Idunn's lease for `warming_sha256` once `count` heartbeats arrived,
/// then stamps when it finished.
fn grant_after(
rig: &Rig,
odin: &Odin,
count: usize,
which: usize,
written: &Mutex<Option<Instant>>,
) {
let delivered = odin.wait_for(count);
let sha = delivered[which].record().canonical_sha256().unwrap();
rig.write_lease(&sha, 1).unwrap();
*written.lock().unwrap() = Some(Instant::now());
}
#[test]
fn lease_wait_heartbeats_and_returns_guard() -> Result<()> {
let odin = Odin::start(Sink::Accept);
let rig = odin_rig(true, &odin.rudp_endpoint())?;
let mut signer = rig.signer()?;
let cadence = Cadence {
heartbeat: Duration::from_millis(300),
..TEST_CADENCE
};
let publisher = publisher_for(&signer, cadence)?;
let written = Mutex::new(None);
let started = Instant::now();
let guard = std::thread::scope(|scope| {
scope.spawn(|| grant_after(&rig, &odin, 2, 1, &written));
publisher.wait_for_write_lease(&mut signer, Duration::from_secs(10))
})?
.expect("the wait returns the lease guard");
let returned = Instant::now();
let delivered = odin.deliveries();
assert_eq!(delivered.len(), 2, "no heartbeat after the lease");
for heartbeat in &delivered {
let record = heartbeat.record();
assert_eq!(record.state, "warming");
assert_eq!(record.write_lease_sha256, None);
}
// The first heartbeat is sent on entry, the next one heartbeat later.
let gap = delivered[1].record().observed_at_unix_millis
- delivered[0].record().observed_at_unix_millis;
assert!((290..600).contains(&gap), "heartbeat gap {gap} ms");
assert!(returned - started >= Duration::from_millis(290));
// The lease is polled far faster than heartbeats are sent.
let written = written.lock().unwrap().expect("the lease was written");
assert!(
returned - written < Duration::from_millis(150),
"the wait noticed the lease after {:?}",
returned - written
);
// The guard is the one the signer now signs under.
signer.report_health(RuntimePresenceState::Active);
let active = publisher.publish(&mut signer, "ready")?;
assert_eq!(
active.record.write_lease_sha256.as_deref(),
Some(guard.lease_sha256())
);
Ok(())
}
#[test]
fn lease_wait_refuses_non_warming_signer() -> Result<()> {
let odin = Odin::start(Sink::Accept);
let rig = odin_rig(true, &odin.rudp_endpoint())?;
let mut signer = rig.signer()?;
let publisher = publisher_for(&signer, TEST_CADENCE)?;
signer.report_health(RuntimePresenceState::Active);
let refused = refusal(publisher.wait_for_write_lease(&mut signer, Duration::from_secs(5)));
assert!(refused.contains("Warming"), "{refused}");
assert_eq!(odin.count(), 0, "refused before any publish");
Ok(())
}
#[test]
fn lease_wait_survives_failed_heartbeat() -> Result<()> {
let addr = dead_address();
let rig = odin_rig(true, &format!("rudp://{addr}"))?;
let mut signer = rig.signer()?;
let cadence = Cadence {
connect: Duration::from_millis(60),
..TEST_CADENCE
};
let publisher = publisher_for(&signer, cadence)?;
let written = Mutex::new(None);
let started = Instant::now();
let guard = std::thread::scope(|scope| {
// Odin comes up after the first heartbeats failed.
let late = scope.spawn(|| {
std::thread::sleep(Duration::from_millis(350));
let odin = Odin::start_on(addr, Sink::Accept);
grant_after(&rig, &odin, 1, 0, &written);
odin
});
let guard = publisher.wait_for_write_lease(&mut signer, Duration::from_secs(10));
late.join().unwrap();
guard
})?;
assert!(guard.is_some());
assert!(started.elapsed() >= Duration::from_millis(350));
Ok(())
}
#[test]
fn lease_wait_times_out_with_counts() -> Result<()> {
let dead = dead_address();
let rig = odin_rig(true, &format!("rudp://{dead}"))?;
let mut signer = rig.signer()?;
let cadence = Cadence {
heartbeat: Duration::from_millis(50),
connect: Duration::from_millis(30),
..TEST_CADENCE
};
let publisher = publisher_for(&signer, cadence)?;
let started = Instant::now();
let refused = refusal(publisher.wait_for_write_lease(&mut signer, Duration::from_millis(500)));
let elapsed = started.elapsed();
assert!(elapsed >= Duration::from_millis(500), "{elapsed:?}");
assert!(elapsed < Duration::from_millis(2_000), "{elapsed:?}");
let count: u32 = refused
.split(" failed deliveries")
.next()
.and_then(|head| head.rsplit(' ').next())
.and_then(|count| count.parse().ok())
.unwrap_or_else(|| panic!("no failure count in {refused}"));
assert!((3..=11).contains(&count), "{count} failed deliveries");
assert!(refused.contains("connect_timeout"), "{refused}");
assert!(!refused.contains("127.0.0.1"), "{refused}");
assert!(!refused.contains(&dead.port().to_string()), "{refused}");
assert!(
!refused.contains(rig.dir.path().to_str().unwrap()),
"{refused}"
);
Ok(())
}
#[test]
fn lease_wait_timeout_counts_only_failed_deliveries() -> Result<()> {
let odin = Odin::start(Sink::Accept);
let rig = odin_rig(true, &odin.rudp_endpoint())?;
let mut signer = rig.signer()?;
let cadence = Cadence {
heartbeat: Duration::from_millis(50),
..TEST_CADENCE
};
let publisher = publisher_for(&signer, cadence)?;
let refused = refusal(publisher.wait_for_write_lease(&mut signer, Duration::from_millis(400)));
assert!(refused.contains(": 0 failed deliveries"), "{refused}");
assert!(!refused.contains("last:"), "{refused}");
assert!(odin.count() >= 3, "heartbeats kept arriving");
Ok(())
}
#[test]
fn lease_wait_ends_at_once_on_a_foreign_incarnation_lease() -> Result<()> {
let odin = Odin::start(Sink::Accept);
let rig = odin_rig(true, &odin.rudp_endpoint())?;
let mut signer = rig.signer()?;
let publisher = publisher_for(&signer, TEST_CADENCE)?;
let started = Instant::now();
let refused = std::thread::scope(|scope| {
scope.spawn(|| {
let sha = odin.wait_for(1)[0].record().canonical_sha256().unwrap();
// Staged elsewhere, so the wait never sees a lease that binds it.
let staged = rig.dir.path().join("staged-lease.cc");
rig.write_lease_at(&staged, &sha, 1).unwrap();
let stored = SingleFileMessagePackBackingStore::new(&staged)
.pull_all()
.unwrap()
.remove(0);
let mut lease =
IdunnProcessWriteLeaseRecord::decode_canonical(&stored.payload).unwrap();
lease.runtime_instance_id = digest('1');
write_record(
&rig.lease_path(),
TARGET,
IdunnProcessWriteLeaseRecord::TYPE,
IDUNN_PROCESS_WRITE_LEASE_SCHEMA,
lease.canonical_bytes().unwrap(),
)
.unwrap();
});
refusal(publisher.wait_for_write_lease(&mut signer, Duration::from_secs(10)))
});
assert!(refused.contains("provider-owned Warming proof"), "{refused}");
assert!(started.elapsed() < Duration::from_secs(3));
Ok(())
}
}