Node Crate
calimero-node + calimero-node-primitives
Purpose
NodeManager is an Actix actor that orchestrates the entire node. It receives network events via a dedicated channel bridge, manages blob caching, periodic heartbeats, delta handling, and routes streams to the sync manager.
It does not run libp2p directly — NetworkManager does that. NodeManager communicates with the network through NodeClient, which wraps a LazyRecipient<NodeMessage> and a NetworkClient handle. The crate also contains the SyncManager (a long-lived async task, not an Actix actor) and a GarbageCollector actor for storage tombstone cleanup.
Module Structure
File layout across the two crates. Each handler lives in its own focused file following the Single Responsibility Principle.
calimero-node
calimero-node / sync
None scope_root, the manager now computes a scope_verdict. If the verdict is ScopeVerdict::GovDiverged (local and peer scope roots differ despite identical entity roots), it calls pull_namespace_governance and returns early, bypassing the entity walk entirely. This fills the gap where a governance rotation or ACL change — invisible to the entity Merkle — would otherwise persist uncorrected because the post-sync P6.S2 hook only fires after HC/LevelWise actually runs. The datastore read is gated on peer_scope_root being Some; when a peer sends scope_root: None (cold projection or non-group context) the store is never touched and scope_verdict is never computed. After a successful pre-sync governance pull the manager emits a debug-level gov_divergence_pull_complete marker, pairing with the existing gov_divergence_pull_triggered info log. After a successful HashComparison or LevelWise session, if gov_divergence_detected is true, the ProtocolSelector likewise calls pull_namespace_governance and logs gov_divergence_pull_triggered; previously only a passive warning was emitted with no corrective action. The pull is self-limiting: once governance converges the verdict is no longer GovDiverged and no further pull is issued. Implementors of ProtocolDispatch must implement pull_namespace_governance (delegating to the inherent method).(root_hash, Option<scope_root>)); query_peer_dag_state return type extended from Option<(Hash, Vec<[u8; DIGEST_SIZE]>)> to Option<(Hash, Vec<[u8; DIGEST_SIZE]>, Option<Hash>)> — the scope_root field from DagHeadsResponse (previously discarded as scope_root: _) is now captured and threaded back to all callers; on the re-query convergence path, stats.root_hash_verified is now set via helpers::scope_verdict(...).converged() — the inline match (local_scope_root, peer_scope_root) block is replaced by a call to the shared scope_verdict helper; the scope_root_governance_divergence WARN is emitted by pattern-matching ScopeVerdict::GovDiverged(local, peer) directly, eliminating the prior if let (true, Some(...), Some(...)) guard; HashComparisonStats gains a gov_divergence_detected: bool field set to true when the verdict is ScopeVerdict::GovDiverged, exposing the signal to the ProtocolSelector; the WARN text changes from "awaiting governance sync to propagate the rotation" to "pulling governance from the peer to propagate the rotation"; the deterministic sync simulator sends scope_root: None(root_hash, Option<scope_root>) rather than comparing against the stale handshake root; convergence is now decided via helpers::scope_verdict(...).converged() — the inline match (local_scope_root, peer_scope_root) block is replaced by the shared helper, mirroring HashComparison. Transport faults fall back to the handshake root; an older peer returning Ok(None) falls back silently. The responder loop has an explicit InitPayload::DagHeadsRequest arm: after all leaf/tombstone merges it re-reads the local entity root, folds the governance projection to obtain scope_root (None for non-group or cold projection), and replies with a DagHeadsResponse carrying both fields. LevelWiseStats gains a gov_divergence_detected: bool field set to true when the verdict is ScopeVerdict::GovDiverged, mirroring HashComparisonStats. When root_hash_verified is false, the scope_root_governance_divergence WARN is emitted by pattern-matching ScopeVerdict::GovDiverged(local, peer); the WARN text changes from "awaiting governance sync to propagate the rotation" to "pulling governance from the peer to propagate the rotation"; all other mismatches emit the general non-convergence warning using the freshly re-queried peer root.None for non-group contexts or store faults (TODO(perf, C1+): replace full DAG walk with maintained projection reads). Also defines ScopeVerdict { Converged, GovDiverged([u8;32],[u8;32]), DataDiverged } and the free function scope_verdict(local_scope_root, peer_scope_root, local_entity_root, peer_entity_root) -> ScopeVerdict — the single convergence authority used by both HashComparison and LevelWise protocols, and by the pre-sync SyncProtocol::None divergence check in SyncManager. GovDiverged carries both resolved scope roots directly so callers can pattern-match and log them without re-destructuring Options. When either scope root is None (cold projection or non-group context), scope_verdict falls back to bare entity-root comparison (treating asymmetric None as None/None would cause false GovDiverged alarms on partially-warmed nodes). The pre-sync path gates the store read on peer_scope_root being Some, so the common already-in-sync path for cold/non-group peers incurs no I/O.calimero-node-primitives
None means the responder could not resolve/fold the scope; initiators must skip the shadow comparison in that caseKey Types
Central structs in the node crate. NodeManager is the actor; its fields are split into three injected concerns: clients, managers, and mutable state.
NodeManager
clients: NodeClients, // context + node façades
managers: NodeManagers, // blobstore + sync
state: NodeState, // mutable runtime state
}
NodeClients
context: ContextClient,
node: NodeClient,
}
NodeManagers
blobstore: BlobManager,
sync: SyncManager,
}
NodeState
blob_cache: Arc<DashMap<BlobId, CachedBlob>>,
delta_stores: Arc<DashMap<ContextId, DeltaStore>>,
pending_specialized_node_invites: PendingSpecializedNodeInvites,
accept_mock_tee: bool,
node_mode: NodeMode,
sync_sessions: Arc<DashMap<ContextId, SyncSession>>,
}
NodeConfig
home: Utf8PathBuf,
identity: Keypair,
// namespace identity is auto-generated per root group in the datastore
network: NetworkConfig,
sync: SyncConfig,
datastore: StoreConfig,
blobstore: BlobStoreConfig,
context: ContextConfig,
server: ServerConfig,
gc_interval_secs: Option<u64>,
mode: NodeMode,
specialized_node: SpecializedNodeConfig,
mock_tee: bool, // dev/test only — refused if TeeConfig::has_real_attestation
}
The --mock-tee flag (or MEROD_MOCK_TEE env var) on merod run makes fleet-join and attest handlers produce and accept mock TDX attestation quotes instead of requiring real hardware. When active, a loud tracing::warn is emitted at startup; a second warn fires if a Phala KMS provider is configured but real attestation is disabled (likely misconfiguration). The flag is refused (bail!) when TeeConfig::has_real_attestation() returns true — i.e. when real KMS attestation is configured. NodeConfig::mock_tee threads the value into AdminState (via AdminState::new); it is never persisted to the config file. Must not be used in production.