Local Group Governance

Signed gossip operations, causal DAG, capability-based authorization

Ed25519
signed ops
DAG
causal ordering
5
capability bits
20+
op variants

Architecture

Node A GroupStore Members, capabilities Contexts, aliases DagStore Causal ordering Pending resolution OpLog Persistent sequence of applied ops + DAG heads SignedGroupOp (v3) group_id + parent_op_hashes state_hash + nonce + signer op: GroupOp + Ed25519 signature borsh · content-addressed Node B GroupStore Members, capabilities Contexts, aliases DagStore Causal ordering Pending resolution OpLog Persistent sequence of applied ops + DAG heads gossipsub namespace/<hex> topic
GroupStore (persistence)
DagStore (causal ordering)
OpLog (replay + catch-up)
SignedGroupOp (wire format)

Core Concept: Groups Own Contexts

Group admin · members · policies Context A app + state + identities Context B app + state + identities Context C app + state + identities Admin Member 1 Member 2 Group membership → access to contexts MemberRemoved → cascade removal from all contexts + subgroups

Member Roles

Admin

Full control. Can perform all governance operations, manage members, configure the group, and write state in all contexts.

Member

Standard participant. Can read and write state in joined contexts. Specific governance operations require capability bits (e.g. CAN_INVITE_MEMBERS, MANAGE_MEMBERS).

ReadOnly

Observer. Can join contexts and read state, but state mutations are rejected by the platform. Enforced at both the local node (execute handler discards changes) and remote nodes (state deltas from ReadOnly authors are rejected on ingestion).

Subgroups

Groups can form a tree hierarchy. A child group links to a parent via GroupParentRef. Membership and admin authority inherit downward:

Membership Inheritance

Membership checks walk up the ancestor chain (max depth 16). A member of the parent group is automatically a member of all descendant groups. Direct membership in the child takes priority over inherited membership.

Admin Authority

Parent admins inherit structural governance over descendant groups: add/remove members, detach contexts, delete subgroups, set target application.

Restricting Access via Subgroups

To restrict access to specific contexts, create a subgroup and register those contexts under it. Only members of that subgroup (direct or inherited from its parent) can join its contexts. This replaces the former per-context visibility/allowlist model with a simpler group-membership-based approach.

Create a subgroup with --parent-group-id on meroctl group create or via the parentGroupId field in the API. The SubgroupCreated governance op is published on the parent group's gossip topic.

When joining a parent group with auto_join (default: true), the node subscribes to gossip topics and contexts in all descendant subgroups. MemberRemoved cascades to descendant subgroup contexts (skipping child groups where the member has direct membership).

Governance DAG Structure

Each group has a DAG of SignedGroupOp operations. Operations reference their parents by content hash, forming a causal chain that supports offline ops, concurrent admins, and merge.

G genesis (group create) A1 MemberAdded A2 Admin A A3 B1 Admin B B2 M Merge (Noop with 2 parents) delta_id = SHA-256(signable_bytes) parents = current DAG heads state_hash = optimistic lock Ordering guarantees: • topological (parents first) • pending queue for OOO
Genesis
Admin A ops
Admin B ops (concurrent)
Merge op

SignedGroupOp Schema (v3)

Signable Payload

version
Schema version (currently 3)
group_id
Target group identifier
parent_op_hashes
DAG ancestry (current heads at sign time)
state_hash
Optimistic lock — SHA-256 of group state
signer
Ed25519 public key of the admin
nonce
Per-signer monotonic (dedup + replay)
op: GroupOp
The actual operation (see right column)

GroupOp Variants

Noop
Merge sentinel (no state change, used to merge DAG heads)
MemberAdded
Add member to group (admin or MANAGE_MEMBERS)
MemberRemoved
Remove member + cascade to all contexts in this group and descendant subgroups (admin or MANAGE_MEMBERS)
MemberRoleSet
Change member role (admin only)
MemberCapabilitySet
Set per-member capability bitfield (admin only)
DefaultCapabilitiesSet
Set default capabilities for new members (admin only)
UpgradePolicySet
Set group upgrade policy (admin only)
TargetApplicationSet
Set target application for the group (admin or MANAGE_APPLICATION)
ContextRegistered
Bind a new context to the group (admin or CAN_CREATE_CONTEXT)
ContextDetached
Remove context from group (admin only)
ContextAliasSet
Set human-readable context alias (admin or context creator)
MemberAliasSet
Set human-readable member alias (admin or self)
GroupAliasSet
Set human-readable group alias (admin only)
GroupDelete
Delete the group (admin only, requires no contexts)
GroupMigrationSet
Configure migration parameters (admin or MANAGE_APPLICATION)
JoinWithInvitationClaim
Invitation-based group join (inviter must be admin or CAN_INVITE_MEMBERS)
ContextCapabilityGranted
Grant per-context capability to a member (admin or MANAGE_MEMBERS)
ContextCapabilityRevoked
Revoke per-context capability from a member (admin or MANAGE_MEMBERS)
SubgroupCreated
Link a child group under this group (admin only)
SubgroupRemoved
Unlink a child group from this group (admin only)

Operation Flows

Create Context Flow

1

Admin calls create_context

API receives group_id (or auto-creates group). Uses the node's namespace identity keypair (from the datastore) if no explicit secret provided.

2

Handler signs GroupOps

Emits a sequence: ContextRegistered → optional ContextAliasSet. Each is a SignedGroupOp applied locally and published via gossip.

3

Local apply + OpLog

group_store::apply_local_signed_group_op verifies signature, checks state_hash, updates KV rows, appends to op log, recomputes DAG heads.

4

Gossip propagation

Published on group/<hex> topic. Remote nodes receive via SignedGroupOpV1 broadcast, verify, and apply the same ops.

Join Group via Invitation

1

Admin creates invitation

create_group_invitation generates a SignedGroupOpenInvitation with optional expiration timestamp. Shared out-of-band to the joiner.

2

Joiner submits invitation

join_group builds GroupRevealPayloadData, signs it, submits a JoinWithInvitationClaim GroupOp.

3

Op applied + member added

Group store validates the claim, adds the new member, updates op log. Gossip ensures all peers converge.

Join Context (within Group)

1

Group membership implies context access

Context membership is implicit from group membership — no separate governance op or explicit join is required. When a member joins a group with auto_join: true (default), they automatically subscribe to all contexts in that group and its descendant subgroups.

2

Context identity resolved

The node uses its namespace identity as the context member identity. No per-context keypair generation needed.

Member Removal Cascade

MemberRemoved Remove from group GroupMember row deleted Context A identity removed Context B identity removed Join tracking cleaned up Deterministic: same op on any node produces identical cascade — verified by convergence tests

Sync & Catch-up Protocol

Node A (online) Node B (rejoining) every 30s StateHeartbeat { group_id, dag_heads: [h1, h2] } Compare heads missing h2? Stream request GroupDeltaRequest { delta_id: h2 } read_op_log scan for hash GroupDeltaResponse { payload: borsh(SignedGroupOp) } apply_signed_ group_op converged ✓

Startup Recovery

1

reload_group_dags + recover_in_progress_upgrades

On node start, for each group in the store, reads the full OpLog and rebuilds the in-memory DagStore via restore_applied_delta. Reconstructs pending parent resolution state. Re-spawns propagators for any in-progress group upgrades (crash recovery).

2

start_namespace_heartbeat

Starts a 30-second periodic timer. Each tick collects unique namespaces from all groups and publishes NamespaceStateHeartbeat with current namespace DAG dag_heads. Also publishes GroupStateHeartbeat per group for backward compat.

3

Peers respond with deltas

Peers compare heads against their own. For any ops the restarted node is missing, they serve them via NamespaceBackfillRequest/Response streams (or legacy GroupDeltaRequest/Response).

Namespace Governance

Since the namespace identity model, governance has moved to a single DAG per namespace (root group). All groups within a namespace share one governance DAG, stored in NamespaceGovOp / NamespaceGovHead storage keys. Operations are either cleartext RootOps (visible to all namespace members) or encrypted GroupOps (only readable by group members).

RootOps (Cleartext)

Visible to all namespace members. Used for structural changes and key distribution:

GroupCreated
Atomically creates a new group AND nests it under parent_id (strict-tree invariant — no orphan creation path)
GroupDeleted
Cascade-deletes a group plus its entire subtree and all contained contexts in one op. Payload carries cascade_group_ids + cascade_context_ids which every peer re-enumerates and verifies for deterministic application.
GroupReparented
Atomic edge swap: moves a child from its current parent to a new parent within the same namespace. Replaces the previous GroupNested + GroupUnnested pair; orphan state is structurally impossible.
MemberJoined
Cleartext join notification (joiner doesn’t have group key yet)
KeyDelivery
ECDH-wrapped group key delivery to a specific member
AdminChanged
Changes namespace admin
PolicyUpdated
Extensible policy mutations

GroupOps (Encrypted)

Encrypted with the group’s key. Non-members store an opaque skeleton (causal structure preserved, content unknown):

Group { group_id, key_id, encrypted }
Wrapper containing an encrypted inner GroupOp

Inner GroupOp variants (after decryption) are the same as the per-group ops: MemberAdded, MemberRemoved, ContextRegistered, etc.

Key Delivery Mechanism

1. Joiner publishes RootOp::MemberJoined (signed by joiner’s namespace identity)

2. Existing member sees MemberJoined on the namespace DAG

3. Member wraps group key via ECDH: shared = ECDH(sender_sk, joiner_pk)

4. Member publishes RootOp::KeyDelivery { envelope }

5. Joiner unwraps using ECDH(joiner_sk, sender_pk)

6. Joiner can now decrypt GroupOp payloads and participate fully

Signing Key Hierarchy (Ancestor Walk)

Signing keys are stored at the namespace root when a namespace is created. Child groups (created via create_group_in_namespace) do not get their own copy. Instead, governance_preflight resolves signing authority by walking the parent chain:

1. Check current group for a signing key matching the requester identity

2. If not found, walk to parent via get_parent_group()

3. Repeat up to MAX_NAMESPACE_DEPTH (16) levels + final check at root

4. Return the first key found, or error if none exists

Revocation: Revoking a signing key at the namespace root automatically prevents all descendant groups from signing — no stale copies to clean up. Unnesting: breaks the parent link, so the child can no longer walk to the ancestor’s key.

Recursive Member Removal

When a member is removed from a group, recursive_remove_member() cascades the removal to all descendant groups and their contexts. Removal from a child group does not affect the parent (upward isolation).

Namespace Governance Epoch

Context state deltas carry a governance_epoch field. This is now computed from the namespace DAG heads (not per-group heads), ensuring a consistent governance state reference across all groups in a namespace. Computed via compute_namespace_governance_epoch(store, context_id).

Known Limitations

Multi-admin convergence

Concurrent ops from multiple admins with the same state_hash: first applied wins per node. Different nodes may pick different winners for conflicting ops. Merge Noop resolves heads but op ordering may differ. Single-admin groups have no ambiguity.

Governance epoch

State deltas carry a governance_epoch field but it's not yet used to reject stale application-level deltas from removed members.

Context-level capability enforcement

Context-level capabilities (ManageApplication, ManageMembers) are stored and can be granted/revoked via governance ops, but are not yet checked during WASM execution. ReadOnly role enforcement is platform-level (execute handler + delta ingestion), not capability-based.

Gossip publish failures

If gossip publish fails, it's currently silent. Should surface errors to callers or retry.

Key File Map

Core Governance

context/primitives/src/local_governance/mod.rs
Wire types: SignedGroupOp, GroupOp, SignableGroupOp, SignedNamespaceOp, NamespaceOp (RootOp/GroupOp). Ed25519 signing/verification, content hashing, schema v3.
context/src/governance_dag.rs
GroupGovernanceApplier and NamespaceGovernanceApplier bridge calimero_dag: converts ops to CausalDelta, delegates apply to group_store.
context/src/group_store/mod.rs
Authoritative persistence (refactored into modules). apply_local_signed_group_op handles all GroupOp variants, manages op log, DAG heads, nonces, cascades.
context/src/group_store/namespace.rs
Namespace identity resolution: resolve_namespace(), get_or_create_namespace_identity_bundle(), parent chain walking.
context/src/group_store/namespace_governance.rs
Namespace governance DAG application: apply_signed_namespace_op(), key unwrapping, skeleton storage for encrypted ops.
context/src/group_store/namespace_membership.rs
Namespace membership tracking and policy enforcement.
context/src/lib.rs
ContextManager actor. Holds per-namespace DagStores, runs recovery on startup, starts namespace heartbeat timer.
context/src/lifecycle.rs
Startup recovery: recover_in_progress_upgrades(), start_namespace_heartbeat() (30s periodic).
context/src/metrics.rs
Prometheus metrics: execution count/duration, namespace retry/decode events, membership policy rejections.

Storage Layer

store/src/key/group/mod.rs
26+ typed key prefixes for Column::Group: GroupMeta, GroupMember, OpLog, OpHead, Nonce, Context indexes, capabilities, aliases, NamespaceIdentity (0x37), NamespaceGovOp (0x38), NamespaceGovHead (0x39), GroupKeyEntry (0x3A), GroupParentRef (0x36), GroupChildIndex (0x35).
store/src/types/group.rs
Value types: GroupOpHeadValue, GroupMetaValue, GroupUpgradeValue, NamespaceIdentityValue, NamespaceGovHeadValue.

Network Integration

node/src/handlers/network_event/namespace.rs
Namespace gossip ingestion: NamespaceGovernanceDelta, NamespaceStateHeartbeat → apply ops, trigger key delivery, or backfill via P2P streams.
node/src/handlers/network_event/specialized.rs
Specialized node discovery, TEE attestation, join confirmation handling.
node/src/key_delivery.rs
Automatic key delivery: on MemberJoined, wraps group key via ECDH and publishes RootOp::KeyDelivery.
node/src/sync/manager/mod.rs
Responds to GroupDeltaRequest and NamespaceBackfillRequest by scanning op log and sending responses.

Handlers

context/src/handlers/create_context.rs
Emits ContextRegistered + optional alias as signed ops. Uses namespace identity for signing.
context/src/handlers/join_group.rs
Invitation claim flow: generates/retrieves namespace identity, subscribes to namespace topic, stream-requests join, unwraps group key, applies governance ops snapshot, auto-joins contexts.
context/src/handlers/apply_signed_namespace_op.rs
Namespace DagStore integration: feeds namespace ops into the namespace-level DAG.
context/src/handlers/apply_signed_group_op.rs
Group DagStore integration: feeds op into DAG, handles pending/applied/error states.
context/src/handlers/list_namespaces.rs
Namespace listing, querying, and identity resolution for the admin API.
context/src/handlers/upgrade_group.rs
Application upgrade propagation across group contexts.

Tests

context/src/group_store/tests.rs
Comprehensive group store tests including namespace identity, governance, key delivery, and membership policy.
context/tests/local_group_governance_convergence.rs
Two-node convergence, op log, idempotency, DAG ordering, state_hash conflict prevention, cascade removal, head capping.
node/src/local_governance_node_e2e.rs
E2E: gossip message → node manager → context apply → membership verified.