Wire Protocol

Every message type with field-level documentation

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

BroadcastMessage gossipsub 8 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>.

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) }

GroupMutationKind 18 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) VisibilityUpdated, // context or default visibility changed 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 ContextVisibilitySet, // per-context visibility changed DefaultVisibilitySet, // default visibility for new contexts changed ContextAllowlistSet, // per-context allowlist replaced 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 11 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 }

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 }
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 24 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 } MemberJoinedViaContextInvitation { context_id: ContextId, // context from the invitation inviter_id: PublicKey, // inviter (must be group admin) invitation_payload: Vec<u8>, // borsh(InvitationFromMember) for verification inviter_signature: String, // hex signature over invitation payload }
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 }
Visibility & Access Control — 5 variants
DefaultVisibilitySet { visibility: Visibility, // Open | Restricted — default for new contexts } ContextVisibilitySet { context_id: ContextId, // target context visibility: Visibility, // Open | Restricted } ContextAllowlistReplaced { context_id: ContextId, // target context allowlist: Vec<PublicKey>, // full replacement allowlist } 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

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>