Wire Protocol

Every message type with field-level documentation

10
gossipsub variants
13
stream init types
24
GroupOp variants
gossipsub pub/sub broadcast
stream point-to-point
req/resp request-response
internal actor messages

BroadcastMessage gossipsub 10 variants

Top-level enum for all messages published via gossipsub topics. Each variant is Borsh-serialized before being sent as a gossipsub message payload. Context topics use context/<hex>, group topics use group/<hex>, namespace topics use namespace/<hex>.

StateDelta — application state replication

Carries a WASM execution result (state diff) to all peers in a context. Published on context gossipsub topics after a successful method execution and state commit.

struct StateDelta { context_id: ContextId, // target context author_id: PublicKey, // who executed the method delta_id: [u8; 32], // content-addressed hash of this delta parent_ids: Vec<[u8; 32]>, // DAG ancestry (current heads at produce time) hlc: HybridTimestamp, // hybrid logical clock for causal ordering root_hash: Hash, // Merkle root after applying this delta artifact: Cow<[u8]>, // the actual state diff payload (opaque bytes) nonce: Nonce, // deduplication / replay protection events: Option<Cow<[u8]>>, // optional WASM-emitted events governance_epoch: Vec<[u8; 32]>, // governance DAG heads at delta time (future: stale check) }
Borsh serialization · delta_id = SHA-256(artifact + metadata) · parent_ids capped by DAG head limit
HashHeartbeat — context state comparison

Periodic heartbeat published on context topics. Peers compare root_hash and dag_heads to detect divergence and trigger sync.

struct HashHeartbeat { context_id: ContextId, // which context root_hash: Hash, // current Merkle root of context state dag_heads: Vec<[u8; 32]>, // current DAG head delta IDs }
SpecializedNodeDiscovery — peer role announcement
struct SpecializedNodeDiscovery { nonce: Nonce, // unique per announcement (dedup) node_type: NodeType, // e.g. Sequencer, Indexer, Relay }
SpecializedNodeJoinConfirmation — role join ack
struct SpecializedNodeJoinConfirmation { nonce: Nonce, // matches the discovery nonce }
GroupMutationNotification — group change signal

Lightweight notification that a group was mutated. Does not contain the full op — peers use this to decide whether to fetch details or update caches.

struct GroupMutationNotification { group_id: GroupId, // affected group mutation_kind: GroupMutationKind, // what kind of mutation (see below) }
SignedGroupOpV1 — governance operation (v1 wire)

Carries a Borsh-serialized SignedGroupOp as an opaque byte vector. Published on group gossipsub topics. Receivers deserialize, verify signature, and apply.

struct SignedGroupOpV1 { payload: Vec<u8>, // borsh(SignedGroupOp) — opaque wire format }
Inner payload is borsh(SignedGroupOp) — see SignedGroupOp schema section below for full structure
GroupGovernanceDelta — governance DAG delta

Alternative governance replication format used by the DAG sync layer. Carries the same data as SignedGroupOpV1 but with explicit DAG metadata for the governance DAG.

struct GroupGovernanceDelta { group_id: GroupId, // target group delta_id: [u8; 32], // content hash of this governance delta parent_ids: Vec<[u8; 32]>, // governance DAG parents payload: Vec<u8>, // borsh(SignedGroupOp) }
GroupStateHeartbeat — group governance comparison

Periodic heartbeat for governance DAG state. Published every 30s on group topics. Peers compare dag_heads to detect missing ops and trigger governance catch-up.

struct GroupStateHeartbeat { group_id: GroupId, // which group dag_heads: Vec<[u8; 32]>, // current governance DAG head op hashes member_count: u64, // current member count (quick sanity check) }
NamespaceGovernanceDelta — namespace governance op

Carries a SignedNamespaceOp on namespace gossipsub topics. Published when a namespace governance operation is created (group creation, member join, key delivery, etc.). Receivers verify the signature, apply to the namespace DAG, and may trigger key delivery.

struct NamespaceGovernanceDelta { namespace_id: [u8; 32], // target namespace (root group) payload: Vec<u8>, // borsh(SignedNamespaceOp) }
Payload size capped by MAX_SIGNED_GROUP_OP_PAYLOAD_BYTES · Published on namespace/<hex> topic
NamespaceStateHeartbeat — namespace governance comparison

Periodic heartbeat for namespace governance DAG state. Published every 30s on namespace topics. Peers compare dag_heads (capped at 256) to detect missing ops. Split logic: gossipsub for ops the peer needs, P2P stream for backfill of ops we need.

struct NamespaceStateHeartbeat { namespace_id: [u8; 32], // which namespace dag_heads: Vec<[u8; 32]>, // current namespace governance DAG heads }

GroupMutationKind 14 variants

Enum carried inside GroupMutationNotification. Describes what kind of group mutation occurred, allowing receivers to react selectively without deserializing the full op.

enum GroupMutationKind { MembersAdded, // one or more members added MembersRemoved, // one or more members removed (cascade) Upgraded, // group upgrade policy or application changed Deleted, // group deleted entirely ContextDetached, // context unbound from group SettingsUpdated, // general group settings changed MemberRoleUpdated, // member role changed (admin/member) ContextAttached, // new context bound to group ContextAliasSet, // human-readable context alias changed MemberCapabilitySet, // per-member capability grant/revoke DefaultCapabilitiesSet, // default capabilities for new members changed MemberAliasSet, // human-readable member alias changed GroupAliasSet, // human-readable group alias changed ContextRegistered, // new context registered to group }

StreamMessage stream 3 variants

Framing protocol for all point-to-point stream communication. Each stream begins with an Init message, followed by zero or more Message frames, and can terminate with OpaqueError.

enum StreamMessage { Init { payload: InitPayload, // identifies the stream type and initial data }, Message { payload: MessagePayload, // subsequent data frames }, OpaqueError, // unit variant — signals failure without leaking node state }
Borsh serialization · length-prefixed framing over libp2p streams (QUIC/TCP)

InitPayload stream init 13 variants

Sent as the first message on a new stream. Determines the protocol/purpose of the stream and carries initial handshake data.

BlobShare — transfer application binary
struct BlobShare { blob_id: BlobId, // identifier of the WASM blob blob: Cow<[u8]>, // the actual blob bytes }
KeyShare — exchange encryption key
struct KeyShare { context_id: ContextId, // target context holder_id: PublicKey, // who holds the key encrypted_key: Cow<[u8]>, // encrypted store key material }
DeltaRequest — fetch specific delta
struct DeltaRequest { context_id: ContextId, // which context delta_id: [u8; 32], // content hash of the requested delta }
DagHeadsRequest — get current heads
struct DagHeadsRequest { context_id: ContextId, // which context's DAG heads to return }
SnapshotBoundaryRequest — negotiate snapshot range
struct SnapshotBoundaryRequest { context_id: ContextId, // target context root_hash: Hash, // requester's current root (may be empty) }
SnapshotStreamRequest — start full snapshot transfer
struct SnapshotStreamRequest { context_id: ContextId, // which context to snapshot }
TreeNodeRequest — fetch Merkle tree node
struct TreeNodeRequest { context_id: ContextId, // target context node_hash: Hash, // hash of the tree node to fetch }
LevelWiseRequest — level-wise tree comparison
struct LevelWiseRequest { context_id: ContextId, // target context level: u32, // which tree level hashes: Vec<Hash>, // node hashes at this level }
EntityPush — push entities to peer
struct EntityPush { context_id: ContextId, // target context entities: Vec<u8>, // serialized entity data }
GroupDeltaRequest — fetch governance op

Requests a specific governance operation from the peer's op log. Used during governance catch-up when heartbeat reveals missing ops.

struct GroupDeltaRequest { group_id: GroupId, // which group delta_id: [u8; 32], // content hash of the requested governance op }
NamespaceBackfillRequest — fetch namespace governance ops

Requests one or more namespace governance operations by delta ID. Opened as a P2P stream when NamespaceStateHeartbeat comparison reveals ops we are missing. The peer responds with NamespaceBackfillResponse messages.

struct NamespaceBackfillRequest { namespace_id: [u8; 32], // which namespace delta_ids: Vec<[u8; 32]>, // content hashes of requested ops }
NamespaceJoinRequest — join namespace via invitation

Sent by a joiner to an existing namespace member after subscribing to the namespace topic and forming a mesh. The peer responds with the group key (wrapped via ECDH), a snapshot of governance ops, and a list of context IDs to auto-join.

struct NamespaceJoinRequest { namespace_id: [u8; 32], // which namespace invitation: SignedGroupOpenInvitation, // proof of valid invitation joiner_pk: PublicKey, // joiner's namespace identity public key }

MessagePayload stream data 14 variants

Subsequent data frames sent after the initial Init on a stream. The variant matches the stream type established by InitPayload.

DeltaResponse — delta fetch result
struct DeltaResponse { delta: Option<StateDelta>, // the requested delta, or None if not found }
DagHeadsResponse — current DAG heads
struct DagHeadsResponse { heads: Vec<[u8; 32]>, // current DAG head delta IDs }
SnapshotBoundaryResponse — snapshot range info
struct SnapshotBoundaryResponse { root_hash: Hash, // sender's current root dag_heads: Vec<[u8; 32]>, // sender's current DAG heads entity_count: u64, // estimated entity count for sizing }
SnapshotStreamData — snapshot chunk
struct SnapshotStreamData { chunk: Cow<[u8]>, // serialized state chunk is_last: bool, // true if this is the final chunk }
SnapshotStreamEnd — snapshot complete
struct SnapshotStreamEnd { root_hash: Hash, // final root hash for verification dag_heads: Vec<[u8; 32]>, // DAG heads at snapshot time }
TreeNodeResponse — Merkle tree node data
struct TreeNodeResponse { node: Option<TreeNode>, // the tree node, or None if not found }
LevelWiseResponse — level comparison result
struct LevelWiseResponse { differing: Vec<Hash>, // hashes that differ at this level missing: Vec<Hash>, // hashes the responder doesn't have }
EntityPushAck — entity push acknowledgment
struct EntityPushAck { accepted: u64, // number of entities accepted rejected: u64, // number of entities rejected }
GroupDeltaResponse — governance op data
struct GroupDeltaResponse { payload: Option<Vec<u8>>, // borsh(SignedGroupOp), or None if not found }
NamespaceBackfillResponse — namespace governance ops
struct NamespaceBackfillResponse { deltas: Vec<Vec<u8>>, // borsh(SignedNamespaceOp) for each requested delta_id }
BlobShareAck — blob received confirmation
struct BlobShareAck { blob_id: BlobId, // confirmed blob accepted: bool, // true if stored, false if already had it }
KeyShareAck — key share confirmation
struct KeyShareAck { accepted: bool, // true if key was stored }
ContextStateCheckResponse — state check result
struct ContextStateCheckResponse { root_hash: Hash, // peer's current root hash in_sync: bool, // whether peer considers itself in sync }
StateSyncChunk — incremental state sync data
struct StateSyncChunk { chunk_id: u64, // sequential chunk index data: Cow<[u8]>, // serialized entities is_last: bool, // final chunk marker }
StateSyncComplete — state sync finalization
struct StateSyncComplete { root_hash: Hash, // final root hash after sync dag_heads: Vec<[u8; 32]>, // DAG heads after sync total_entities: u64, // total entities transferred }

GroupOp gossipsub 20 variants

The actual governance operation inside a SignedGroupOp. Each variant represents a specific group mutation. Applied deterministically on all nodes for convergence.

Member Management — 9 variants
MemberAdded { member_id: PublicKey, // Ed25519 public key of new member role: MemberRole, // Admin | Member capabilities: Capabilities, // initial capability set } MemberRemoved { member_id: PublicKey, // triggers cascade removal from all contexts } MemberRoleSet { member_id: PublicKey, // target member role: MemberRole, // new role } MemberCapabilitySet { member_id: PublicKey, // target member capabilities: Capabilities, // updated capability bitmask } DefaultCapabilitiesSet { capabilities: Capabilities, // default for new members joining } JoinWithInvitationClaim { claim: InvitationClaim, // signed proof of valid invitation } SubgroupCreated { child_group_id: [u8; 32], // link a child group under this group } SubgroupRemoved { child_group_id: [u8; 32], // unlink a child group from its parent }
Context Lifecycle — 3 variants
ContextRegistered { context_id: ContextId, // new context identifier application_id: Option<ApplicationId>, // optional initial WASM app } ContextDetached { context_id: ContextId, // context to unbind from group } // MemberJoinedContext — removed (context membership is now implicit from group membership) // MemberLeftContext — removed (context membership is now implicit from group membership) TargetApplicationSet { application_id: ApplicationId, // default app for new contexts in this group }
Access Control — 2 variants
ContextCapabilityGranted { context_id: ContextId, // target context member_id: PublicKey, // target member capability: Capability, // specific capability granted } ContextCapabilityRevoked { context_id: ContextId, // target context member_id: PublicKey, // target member capability: Capability, // specific capability revoked }
Aliases — 3 variants
ContextAliasSet { context_id: ContextId, // target context alias: Option<String>, // human-readable name (None to clear) } MemberAliasSet { member_id: PublicKey, // target member alias: Option<String>, // human-readable name (None to clear) } GroupAliasSet { alias: Option<String>, // human-readable group name (None to clear) }
Group Lifecycle & Special — 4 variants
GroupDelete { // no fields — deletes the entire group and all bindings } GroupMigrationSet { migration: MigrationConfig, // migration parameters } UpgradePolicySet { policy: UpgradePolicy, // how application upgrades propagate } Noop { // no fields — merge sentinel, no state change // used to reconcile divergent DAG heads // typically has 2+ parent_op_hashes }

SignedGroupOp Schema v3

The complete signed governance operation structure. Each op is content-addressed (SHA-256 of signable bytes), Ed25519 signed, and forms a DAG via parent hashes.

Wire Layout

struct SignedGroupOp { version: u8, // schema version (currently 3) group_id: [u8; 32], // target group identifier parent_op_hashes: Vec<[u8; 32]>, // DAG ancestry (current heads) state_hash: [u8; 32], // optimistic lock on group state signer: PublicKey, // Ed25519 public key of signer nonce: u64, // per-signer monotonic counter op: GroupOp, // the actual operation signature: [u8; 64], // Ed25519 signature }

Signable Subset

struct SignableGroupOp { version: u8, group_id: [u8; 32], parent_op_hashes: Vec<[u8; 32]>, state_hash: [u8; 32], signer: PublicKey, nonce: u64, op: GroupOp, // signature excluded from signing input }

Signing Process

1. Build signable: Construct SignableGroupOp (all fields except signature).

2. Domain prefix: Prepend domain separator b"calimero-signed-group-op-v3" to Borsh-serialized bytes.

3. Sign: signature = Ed25519::sign(private_key, domain_prefix ++ borsh(signable))

4. Content hash: op_hash = SHA-256(borsh(signable)) — used as the DAG delta ID.

Validation Order

  • Signature verification (Ed25519)
  • Schema version check (must be 3)
  • Nonce ≥ last seen nonce for signer
  • state_hash matches current group state (or all-zeros to bypass)
  • Signer has sufficient role/capabilities for the op
  • parent_op_hashes.len() ≤ 256

Key Invariants

  • parent_op_hashes — capped at 256 entries
  • nonce — monotonically increasing per signer
  • state_hash — all-zeros means skip validation
  • max_dag_heads — capped at 64 per group
  • Content hash is deterministic: same signable bytes → same hash

SignedNamespaceOp Schema namespace governance

Namespace-level governance operations. Each namespace has a single DAG shared by all groups within it. Operations are either RootOps (cleartext, visible to all namespace members) or GroupOps (encrypted with the group key, only readable by group members).

NamespaceOp — RootOp Variants

Cleartext operations visible to all namespace members. Used for structural changes and key distribution.

GroupCreated { group_id: [u8; 32], // new group id parent_id: [u8; 32], // REQUIRED — atomic create+nest (strict-tree invariant) } GroupDeleted { root_group_id: [u8; 32], // delete target cascade_group_ids: Vec<[u8; 32]>, // descendants (children-first) cascade_context_ids: Vec<[u8; 32]>, // all contexts in subtree; peers verify by re-enumeration } GroupReparented { child_group_id: [u8; 32], // subtree to move new_parent_id: [u8; 32], // atomic edge swap — replaces old GroupNested + GroupUnnested pair } MemberJoined { member: PublicKey, // joiner's namespace identity signed_invitation: SignedInvitation, // proof of valid invitation } KeyDelivery { group_id: [u8; 32], // target group envelope: KeyEnvelope, // ECDH-wrapped group key for joiner } AdminChanged { new_admin: PublicKey } PolicyUpdated { policy_bytes: Vec<u8> }

NamespaceOp — GroupOp Variant

Encrypted operations only readable by members of the target group. Non-members store an opaque skeleton (preserving causal structure).

Group { group_id: [u8; 32], // target group key_id: [u8; 32], // which group key was used encrypted: Vec<u8>, // encrypted inner GroupOp key_rotation: Option<..>, // optional key rotation } // Inner GroupOp (after decryption): // MemberAdded, MemberRemoved, MemberRoleSet, // ContextRegistered, ContextDetached, // UpgradePolicySet, TargetApplicationSet, // ... (same variants as GroupOp above)

Key Delivery Flow

// 1. Joiner publishes RootOp::MemberJoined // 2. Existing member sees MemberJoined on DAG // 3. Member wraps group key via ECDH: // shared = ECDH(sender_sk, joiner_pk) // envelope = encrypt(shared, group_key) // 4. Member publishes RootOp::KeyDelivery // 5. Joiner unwraps: ECDH(joiner_sk, sender_pk) // 6. Joiner can now decrypt GroupOp payloads

Signing Process (Namespace Ops)

1. Build SignedNamespaceOp with: namespace_id, parent_hashes, state_hash, nonce, op.

2. Sign with the node's namespace identity private key (not the group signing key).

3. Content hash delta_id = SHA-256(signable_bytes) for DAG identity.

NetworkMessage internal 15 variants

The Actix message enum sent to the NetworkManager actor. These are internal to the node — never serialized on the wire. Each variant triggers a specific libp2p operation.

enum NetworkMessage { Publish { topic: TopicHash, data: Vec<u8> }, // gossipsub publish Subscribe { topic: TopicHash }, // join gossipsub topic Unsubscribe { topic: TopicHash }, // leave gossipsub topic OpenStream { peer: PeerId, protocol: StreamProtocol }, // open direct stream SendMessage { stream_id: StreamId, data: Vec<u8> }, // send on open stream CloseStream { stream_id: StreamId }, // close stream ProvideRecord { key: RecordKey, value: Vec<u8> }, // DHT PUT GetRecord { key: RecordKey }, // DHT GET GetProviders { key: RecordKey }, // DHT find providers GetPeers, // list connected peers GetPeerInfo { peer: PeerId }, // get specific peer metadata Bootstrap, // trigger Kademlia bootstrap DialPeer { peer: PeerId, addrs: Vec<Multiaddr> }, // explicit peer dial GetListenAddrs, // get node's listen addresses GetMeshPeers { topic: TopicHash }, // gossipsub mesh peers for topic }

NetworkEvent internal 11 variants

Events emitted by the NetworkManager actor and handled by NodeManager. These represent observed network activity — gossip messages, peer changes, stream events.

enum NetworkEvent { GossipMessage { topic: TopicHash, // which topic it arrived on source: PeerId, // who sent it data: Vec<u8>, // raw payload (deserialize as BroadcastMessage) }, StreamOpened { stream_id: StreamId, // unique stream identifier peer: PeerId, // remote peer protocol: StreamProtocol, // negotiated protocol }, StreamMessage { stream_id: StreamId, // which stream data: Vec<u8>, // raw payload (deserialize as StreamMessage) }, StreamClosed { stream_id: StreamId, // which stream was closed }, PeerConnected { peer: PeerId, // newly connected peer addrs: Vec<Multiaddr>, // peer's observed addresses }, PeerDisconnected { peer: PeerId, // disconnected peer }, TopicSubscribed { topic: TopicHash, // successfully subscribed }, TopicUnsubscribed { topic: TopicHash, // successfully unsubscribed }, RecordFound { key: RecordKey, // DHT key value: Vec<u8>, // DHT value }, ProvidersFound { key: RecordKey, // DHT key providers: Vec<PeerId>, // peers providing this record }, ListenAddrExpired { addr: Multiaddr, // address no longer valid }, }
These are NOT serialized on the wire — they are Actix messages exchanged internally between NetworkManager and NodeManager actors via LazyRecipient<NodeMessage>