Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/mesh-llm-host-runtime/src/mesh/announcements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ impl Node {
})
}

/// Builds the gossip announcement re-broadcast on behalf of an already
/// admitted peer, from that peer's locally tracked state.
pub(crate) fn announcement_from_peer(peer: &PeerInfo) -> PeerAnnouncement {
let latency = peer.display_latency();
PeerAnnouncement {
Expand Down Expand Up @@ -518,6 +520,10 @@ impl Node {
latency_age_ms: Some(latency.age_ms),
latency_observer_id: latency.observer_id,
inference_admission_state: peer.inference_admission_state,
// No live-mesh claimed-log-head state is tracked on `PeerInfo` yet — a
// rebroadcast of a peer we already admitted carries no opinion on
// its claimed log head.
claimed_log_head: None,
}
}

Expand All @@ -533,6 +539,8 @@ impl Node {
})
}

/// Builds this node's own gossip announcement from freshly collected
/// local data.
pub(crate) fn build_local_announcement(&self, data: LocalAnnouncementData) -> PeerAnnouncement {
PeerAnnouncement {
addr: self.endpoint_addr_for_advertisement(),
Expand Down Expand Up @@ -582,6 +590,9 @@ impl Node {
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: data.inference_admission_state,
// No local claimed-log-head source is wired yet — this node never
// advertises its own until a companion process is plumbed in.
claimed_log_head: None,
}
}
}
4 changes: 2 additions & 2 deletions crates/mesh-llm-host-runtime/src/mesh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ pub use node::{
};
pub(crate) use node::{PeerDownReport, peer_down_endpoint_id};
pub(crate) use peer_state::{
ControlListenerLifecycle, DEAD_PEER_TTL, MeshState, PEER_DOWN_REPORTER_COOLDOWN_SECS,
PEER_STALE_SECS, resolve_peer_leaving,
ClaimedLogHead, ControlListenerLifecycle, DEAD_PEER_TTL, MeshState,
PEER_DOWN_REPORTER_COOLDOWN_SECS, PEER_STALE_SECS, resolve_peer_leaving,
};
#[expect(
unused_imports,
Expand Down
20 changes: 20 additions & 0 deletions crates/mesh-llm-host-runtime/src/mesh/peer_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,26 @@ pub struct PeerAnnouncement {
pub(crate) latency_age_ms: Option<u64>,
pub(crate) latency_observer_id: Option<EndpointId>,
pub(crate) inference_admission_state: Option<crate::proto::node::InferenceAdmissionState>,
/// An optional, self-reported claim this peer MAY advertise about the
/// head of its own append-only history. Carried opaquely; never verified
/// by mesh-llm.
pub(crate) claimed_log_head: Option<ClaimedLogHead>,
}

/// A peer's latest self-reported claim about the head of its append-only log
/// — see `ClaimedLogHead` in `node.proto` for the wire shape and the
/// signing-scope note. Carried opaquely: mesh-llm never verifies
/// `claimed_signature` itself, hence the name — a consumer that does verify
/// it may define its own `VerifiedLogHead` type; none exists here. `pub(crate)`
/// to match `PeerAnnouncement::claimed_log_head`, which is also `pub(crate)`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ClaimedLogHead {
pub(crate) log_id: String,
pub(crate) size: u64,
pub(crate) root: Vec<u8>,
pub(crate) timestamp_unix_ms: u64,
pub(crate) claimed_signature: Vec<u8>,
pub(crate) signature_algorithm: String,
}

/// A single direct RTT measurement (e.g. from gossip exchange).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ pub(super) async fn configure_requirement_node(
Ok(())
}

/// Builds a `PeerAnnouncement` fixture carrying the given genesis policy hash
/// and (optionally) a release attestation, for mesh-requirements admission
/// tests.
pub(super) fn requirement_peer_announcement(
sender_seed: u8,
policy: &crate::MeshGenesisPolicy,
Expand Down Expand Up @@ -428,5 +431,6 @@ pub(super) fn requirement_peer_announcement(
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,8 @@ pub(crate) fn assert_requirement_aware_mesh_without_attestation_accepts_valid_di
});
}

/// Asserts that a peer whose release attestation is signed by a key outside
/// the mesh's trusted signer set is rejected, not admitted.
pub(crate) fn assert_mesh_requirements_add_peer_rejects_untrusted_release_signer() {
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime");
runtime.block_on(async {
Expand Down Expand Up @@ -606,6 +608,7 @@ pub(crate) fn assert_mesh_requirements_add_peer_rejects_untrusted_release_signer
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
};

node.add_peer(
Expand All @@ -630,6 +633,8 @@ pub(crate) fn assert_mesh_requirements_add_peer_rejects_untrusted_release_signer
});
}

/// Asserts that a peer whose release attestation signature does not verify
/// against its claimed signer is rejected, not admitted.
pub(crate) fn assert_mesh_requirements_add_peer_rejects_invalid_release_attestation_signature() {
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime");
runtime.block_on(async {
Expand Down Expand Up @@ -699,6 +704,7 @@ pub(crate) fn assert_mesh_requirements_add_peer_rejects_invalid_release_attestat
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
};

node.add_peer(
Expand All @@ -723,6 +729,8 @@ pub(crate) fn assert_mesh_requirements_add_peer_rejects_invalid_release_attestat
});
}

/// Asserts that a peer advertising a genesis policy hash for a different
/// mesh is rejected, not admitted.
pub(crate) fn assert_mesh_requirements_add_peer_rejects_wrong_mesh_id() {
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime");
runtime.block_on(async {
Expand Down Expand Up @@ -789,6 +797,7 @@ pub(crate) fn assert_mesh_requirements_add_peer_rejects_wrong_mesh_id() {
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
};

node.add_peer(
Expand Down
4 changes: 4 additions & 0 deletions crates/mesh-llm-host-runtime/src/mesh/tests/gossip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub(crate) fn test_addr(seed: u8) -> EndpointAddr {
}
}

/// Builds a minimal `PeerAnnouncement` fixture with the given
/// `first_joined_mesh_ts`, for gossip tests that don't care about the rest
/// of the announcement's fields.
pub(crate) fn test_announcement(ts: Option<u64>) -> PeerAnnouncement {
PeerAnnouncement {
addr: test_addr(0x11),
Expand Down Expand Up @@ -64,6 +67,7 @@ pub(crate) fn test_announcement(ts: Option<u64>) -> PeerAnnouncement {
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
}
}

Expand Down
14 changes: 14 additions & 0 deletions crates/mesh-llm-host-runtime/src/mesh/tests/peer_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,9 @@ fn relay_reconnect_controller_applies_cooldown_after_attempt_and_prunes_gone_pee
);
}

/// Builds a minimal `PeerAnnouncement` fixture at the given address, for
/// peer-state tests that don't care about the rest of the announcement's
/// fields.
fn peer_state_test_announcement(addr: EndpointAddr) -> super::PeerAnnouncement {
super::PeerAnnouncement {
addr,
Expand Down Expand Up @@ -403,6 +406,7 @@ fn peer_state_test_announcement(addr: EndpointAddr) -> super::PeerAnnouncement {
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
}
}

Expand Down Expand Up @@ -921,6 +925,8 @@ fn control_frame_rejects_oversize_or_bad_generation() {
);
}

/// Proves a gossip-frame round trip preserves locally scanned model metadata
/// fields on the announcement.
#[test]
fn gossip_frame_roundtrip_preserves_scanned_model_metadata() {
use crate::proto::node::{CompactModelMetadata, ExpertsSummary};
Expand Down Expand Up @@ -1029,6 +1035,7 @@ fn gossip_frame_roundtrip_preserves_scanned_model_metadata() {
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
};

let proto_pa = local_ann_to_proto_ann(&local_ann);
Expand Down Expand Up @@ -1306,6 +1313,8 @@ fn gossip_rejects_sender_id_mismatch_or_invalid_endpoint_len() {
);
}

/// Proves a transitively gossiped update refreshes a peer's metadata fields
/// in place, without dropping unrelated state.
#[test]
fn transitive_peer_update_refreshes_metadata_fields() {
use crate::proto::node::CompactModelMetadata;
Expand Down Expand Up @@ -1391,6 +1400,7 @@ fn transitive_peer_update_refreshes_metadata_fields() {
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
};

apply_transitive_ann(&mut existing, &addr, &ann, make_test_endpoint_id(0xee));
Expand Down Expand Up @@ -1418,6 +1428,8 @@ fn transitive_peer_update_refreshes_metadata_fields() {
assert!(existing.available_model_sizes.is_empty());
}

/// Proves merging a transitively gossiped peer update never discards a
/// richer, already-known direct address in favor of a sparser one.
#[test]
fn transitive_peer_merge_preserves_richer_direct_address() {
use iroh::TransportAddr;
Expand Down Expand Up @@ -1485,6 +1497,7 @@ fn transitive_peer_merge_preserves_richer_direct_address() {
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
};

apply_transitive_ann(&mut existing, &weak_addr, &ann, make_test_endpoint_id(0xee));
Expand Down Expand Up @@ -1553,6 +1566,7 @@ fn transitive_peer_merge_preserves_richer_direct_address() {
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
};
apply_transitive_ann(
&mut existing,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ async fn transitive_peer_update_refreshes_last_mentioned() {
latency_age_ms: None,
latency_observer_id: None,
inference_admission_state: None,
claimed_log_head: None,
};

node.update_transitive_peer(peer_id, &addr, &ann, make_test_endpoint_id(0xee))
Expand Down
90 changes: 90 additions & 0 deletions crates/mesh-llm-host-runtime/src/protocol/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,8 @@ fn proto_cache_affinity_to_local(
.then_some(advertisement)
}

/// Encodes a local `PeerAnnouncement` to its wire representation, after
/// sanitizing it for outbound gossip.
pub(crate) fn local_ann_to_proto_ann(
ann: &PeerAnnouncement,
) -> crate::proto::node::PeerAnnouncement {
Expand Down Expand Up @@ -879,7 +881,51 @@ pub(crate) fn local_ann_to_proto_ann(
.cache_affinity
.as_ref()
.map(local_cache_affinity_to_proto),
claimed_log_head: ann
.claimed_log_head
.as_ref()
.map(local_claimed_log_head_to_proto),
}
}

/// Converts a local `ClaimedLogHead` to its wire form. No validation here:
/// this node is the one asserting the claim, not receiving it.
fn local_claimed_log_head_to_proto(
head: &crate::mesh::ClaimedLogHead,
) -> crate::proto::node::ClaimedLogHead {
crate::proto::node::ClaimedLogHead {
log_id: head.log_id.clone(),
size: head.size,
root: head.root.clone(),
timestamp_unix_ms: head.timestamp_unix_ms,
claimed_signature: head.claimed_signature.clone(),
signature_algorithm: head.signature_algorithm.clone(),
}
}

/// Decodes a remote `ClaimedLogHead`, rejecting (as absent — never a panic,
/// never a partial struct) any field that exceeds the memory-safety bounds
/// below. mesh-llm never verifies `claimed_signature`; this only bounds
/// untrusted remote byte lengths, the same ingest hygiene already applied to
/// `CacheAffinityAdvertisement.salt` and remote model names.
fn proto_claimed_log_head_to_local(
head: &crate::proto::node::ClaimedLogHead,
) -> Option<crate::mesh::ClaimedLogHead> {
if head.log_id.len() > MAX_CLAIMED_LOG_ID_BYTES
|| head.root.len() > MAX_CLAIMED_LOG_ROOT_BYTES
|| head.claimed_signature.len() > MAX_CLAIMED_LOG_SIGNATURE_BYTES
|| head.signature_algorithm.len() > MAX_CLAIMED_LOG_SIGNATURE_ALGORITHM_BYTES
{
return None;
}
Some(crate::mesh::ClaimedLogHead {
log_id: head.log_id.clone(),
size: head.size,
root: head.root.clone(),
timestamp_unix_ms: head.timestamp_unix_ms,
claimed_signature: head.claimed_signature.clone(),
signature_algorithm: head.signature_algorithm.clone(),
})
}

pub(crate) fn build_gossip_frame(
Expand Down Expand Up @@ -914,6 +960,42 @@ const MAX_REMOTE_MODEL_LIST_LEN: usize = 256;
/// under this; anything larger is dropped rather than rendered.
const MAX_REMOTE_MODEL_NAME_BYTES: usize = 512;

/// Upper bound on `ClaimedLogHead.log_id`, in bytes. This is a memory-safety
/// limit on untrusted remote bytes, not a format assertion: a log id is not
/// a model name, so it gets its own constant rather than borrowing
/// `MAX_REMOTE_MODEL_NAME_BYTES`, sized to match that existing remote-string
/// cap.
const MAX_CLAIMED_LOG_ID_BYTES: usize = 512;

/// Upper bound on `ClaimedLogHead.root`, in bytes. This is a memory-safety
/// limit on untrusted remote bytes, not a format assertion: mesh-llm treats
/// the root as opaque and never verifies it, so the bound is sized generously
/// enough to admit hash schemes wider than this node's own SHA-256 (e.g.
/// SHA-512) rather than asserting our own scheme's exact length.
const MAX_CLAIMED_LOG_ROOT_BYTES: usize = 64;

/// Upper bound on `ClaimedLogHead.claimed_signature`, in bytes. Sized for
/// this node's own Ed25519 (64 bytes) and classical schemes of similar
/// size, with headroom for encoding overhead — **not** for post-quantum
/// schemes: ML-DSA-65 signatures are 3,309 bytes (NIST FIPS 204 gives the
/// ML-DSA range as 2,420-4,627 bytes), all of which this bound rejects.
/// That is deliberate, not an oversight: `claimed_log_head` rides on
/// `PeerAnnouncement`, which is gossiped to every peer, and this node never
/// verifies `claimed_signature` (see `proto_claimed_log_head_to_local`
/// above) — so a bound wide enough to admit a real PQ signature would put
/// multi-kilobyte unverified blobs on a hot broadcast path. Accepting PQ
/// claims would need a deliberate bound raise with its own rationale, not
/// a default this constant already provides.
const MAX_CLAIMED_LOG_SIGNATURE_BYTES: usize = 128;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Upper bound on `ClaimedLogHead.signature_algorithm`, in bytes. Same
/// memory-safety rationale as the other `ClaimedLogHead` bounds: this is an
/// untrusted remote string, not a known-set validator (mesh-llm never
/// verifies the claim, so it has no fixed list of algorithm names to check
/// against). 32 bytes comfortably fits real scheme identifiers (e.g.
/// `"ed25519"`, `"ml-dsa-65"`) while still capping the field.
const MAX_CLAIMED_LOG_SIGNATURE_ALGORITHM_BYTES: usize = 32;

/// Sanitize a remotely-supplied list of model names: drop entries that exceed
/// the per-name byte cap and keep at most `MAX_REMOTE_MODEL_LIST_LEN` of them.
fn cap_remote_model_names(names: &[String]) -> Vec<String> {
Expand All @@ -925,6 +1007,10 @@ fn cap_remote_model_names(names: &[String]) -> Vec<String> {
.collect()
}

/// Decodes a wire `PeerAnnouncement` into its local representation, applying
/// every remote-bytes sanitization boundary (model names, cache affinity,
/// claimed log head) along the way. Returns `None` if the endpoint id itself
/// is malformed.
pub(crate) fn proto_ann_to_local(
pa: &crate::proto::node::PeerAnnouncement,
) -> Option<(EndpointAddr, PeerAnnouncement)> {
Expand Down Expand Up @@ -1122,6 +1208,10 @@ pub(crate) fn proto_ann_to_local(
.cache_affinity
.as_ref()
.and_then(proto_cache_affinity_to_local),
claimed_log_head: pa
.claimed_log_head
.as_ref()
.and_then(proto_claimed_log_head_to_local),
};
crate::mesh::backfill_legacy_descriptors(&mut ann);
ann.advertised_model_throughput = sanitize_model_throughput_hints_for_ann(&ann);
Expand Down
2 changes: 2 additions & 0 deletions crates/mesh-llm-host-runtime/src/protocol/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Protocol infrastructure — extracted from mesh.rs

#[cfg(test)]
use crate::mesh::ClaimedLogHead;
#[cfg(test)]
use crate::mesh::NodeRole;
use crate::mesh::PeerAnnouncement;
Expand Down
Loading
Loading