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, visibility 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, visibility DagStore Causal ordering Pending resolution OpLog Persistent sequence of applied ops + DAG heads gossipsub group/<hex> topic
GroupStore (persistence)
DagStore (causal ordering)
OpLog (replay + catch-up)
SignedGroupOp (wire format)

Core Concept: Groups Own Contexts

Group admin · members · policies · visibility 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. However, parent admins do not inherit access control over Restricted contexts. Only direct group admins (or the context creator) can manage allowlists and change context visibility.

Access Control for Restricted Contexts

Joining

Admins and members alike must be on the allowlist. No admin bypass. An admin can add themselves to the allowlist (if they are a direct admin of the group), then join.

Allowlist Management

Only direct group admins or the context creator. Inherited parent admins cannot modify allowlists in child groups — this is a privacy boundary.

Visibility Changes

Only direct group admins or the context creator can change a context between Open and Restricted. Parent admins cannot flip Restricted to Open as a backdoor.

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, 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)
DefaultVisibilitySet
Set default visibility for new contexts (admin only)
ContextVisibilitySet
Set context visibility: Open or Restricted (admin or context creator)
ContextAllowlistReplaced
Replace per-context member allowlist (admin or context creator)
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)
MemberJoinedViaContextInvitation
Join via context-level invitation (inviter must be admin or CAN_INVITE_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 node's group identity keypair if no explicit secret provided.

2

Handler signs GroupOps

Emits a sequence: ContextRegisteredContextVisibilitySet → 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

Member calls join_group_context

Must already be a group member. Handler checks context visibility (open vs restricted) and member capabilities (CAN_JOIN_OPEN_CONTEXTS).

2

Allowlist check (if restricted)

For restricted-visibility contexts, the member's public key must be on the allowlist.

3

Context identity created

Adds local context membership. Context membership is implicit from group membership — no separate governance op is required.

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

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.

2

start_group_heartbeat

Starts a 30-second periodic timer. Each tick publishes GroupStateHeartbeat with current dag_heads for every group.

3

Peers respond with deltas

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

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.rs
Wire types: SignedGroupOp, GroupOp, SignableGroupOp. Ed25519 signing/verification, content hashing, schema v3.
context/src/governance_dag.rs
GroupGovernanceApplier bridges calimero_dag: converts SignedGroupOp to CausalDelta, delegates apply to group_store.
context/src/group_store.rs
Authoritative persistence. apply_local_signed_group_op handles all GroupOp variants, manages op log, DAG heads, nonces, cascades.
context/src/lib.rs
ContextManager actor. Holds per-group DagStores, runs reload_group_dags on startup, starts heartbeat timer.

Storage Layer

store/src/key/group.rs
20+ typed key prefixes for Column::Group: GroupMeta, GroupMember, OpLog, OpHead, Nonce, Context indexes, capabilities, aliases.
store/src/types/group.rs
Value types: GroupOpHeadValue (sequence + dag_heads), GroupMetaValue, GroupUpgradeValue.

Network Integration

node/src/handlers/network_event.rs
Gossip ingestion: SignedGroupOpV1, GroupGovernanceDelta, GroupStateHeartbeat → apply or stream-request missing ops.
node/src/sync/manager.rs
Responds to GroupDeltaRequest by scanning op log and sending GroupDeltaResponse.

Handlers

context/src/handlers/create_context.rs
Emits ContextRegistered + VisibilitySet + optional alias as signed ops.
context/src/handlers/join_group.rs
Invitation claim flow: validate SignedGroupOpenInvitation, build reveal payload, submit JoinWithInvitationClaim.
context/src/handlers/join_group_context.rs
Context membership with visibility/capability checks.
context/src/handlers/apply_signed_group_op.rs
DagStore integration: feeds op into DAG, handles pending/applied/error states.
context/src/handlers/upgrade_group.rs
Application upgrade propagation across group contexts.

Tests

context/tests/local_group_governance_convergence.rs
21 tests: 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.