Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

RBAC System Design

Version: 2.0 (redesign) Status: Design Supersedes: spec.md RBAC section, task 002/003 RBAC designs


1. Overview

The RBAC system controls authorization for all events in an enclave. Every event — content events (message), AC events (Move, Grant, Revoke), KV events (Shared, Own), and lifecycle events (Pause, Terminate) — is authorized through the manifest. See Section 5 for manifest format and Section 6 for validation rules.

1.1 Terminology

The old unified "role" is retired. Three column types replace it:

ConceptConventionExamplesEncodingChanged bySemantics
StateUPPER_CASEPENDING, MEMBER, BLOCKED8-bit enumMoveWHERE you are. Base permissions. Mutually exclusive.
traitlower_caseowner, admin, mutedFlag bitsGrant / Revoke / Transfer / initWHAT modifies you. Additive or deny ops. Ranked.
ContextPascalCaseSelf, Sender, PublicSystem-evaluated(implicit)System condition. Determined at authorization time.

1.2 Operations

Six operations apply to all event types:

OpMeaningDeny
CCreate_C
RRead_R
UUpdate_U
DDelete_D
NNotify (lightweight, human clients)_N
PPush (full delivery, service endpoints)_P

Positive ops grant capabilities. Negative (deny) ops revoke capabilities. The sign comes from the schema entry, not the bitmask.


2. Bitmask Encoding

All RBAC state for an identity is stored in a single bitmask value in the SMT (namespace 0x00), keyed by the identity's public key.

bits 0-7:    State enum
bits 8+:     trait flags

2.1 State Enum (bits 0-7)

8-bit integer field. All 8 bits together encode a single value. Mutually exclusive by structure — an identity is in exactly one State.

ValueNameDescription
0OUTSIDERNot in the enclave. No SMT leaf exists. Protocol-reserved.
1-255(app-defined)Custom States defined in manifest. E.g., 1=PENDING, 2=MEMBER, 3=BLOCKED.

OUTSIDER (0) is the only protocol-reserved State. All other States — including blocking/banning — are app-defined.

State values 1-255 are assigned sequentially from the manifest states array. The first State in the array gets value 1, the second gets value 2, etc.

2.2 trait Flags (bits 8+)

Independent flag bits. Each bit represents one trait. Multiple traits can be set simultaneously.

Bit positions are assigned sequentially from the manifest traits array: the first trait gets bit 8, the second gets bit 9, etc.

Each trait declares a rank using the syntax name(N) where N is a non-negative integer. Lower number = higher rank. Multiple traits may share the same rank (peers). Rank determines targeting authority: an operator can only target identities ranked strictly below them (see §7 Rank Rule).

All traits are managed uniformly:

  • Assigned via init, Grant, or Transfer.
  • Removed via Revoke, Transfer, or Move (clears all trait flags unless preserve: true).
  • Rank determines targeting authority (lower number = higher authority).

The bitmask does not distinguish positive from negative traits. A "muted" trait flag at bit 10 is stored identically to an "admin" trait flag at bit 9. The sign (positive or negative ops) comes from the schema entry, not the bitmask.

2.3 SMT Leaf Lifecycle

  • Join: Move(OUTSIDER, X) creates a new SMT leaf with State=X and no trait flags.
  • State change: Move(X, Y) updates the State enum and clears all trait flags (unless preserve: true).
  • Leave: Move(X, OUTSIDER) sets State=0 and clears all traits. If bitmask becomes 0, leaf is deleted.
  • Service account: Granting a trait to an OUTSIDER creates a leaf with State=0 and trait bit set.
  • Leaf deletion: SMT leaf is deleted when the entire bitmask == 0 (no State, no traits).
  • No leaf: An identity with no SMT leaf is implicitly OUTSIDER with no traits.

Historical membership is provable via the CT log.


3. Contexts

Contexts are system-evaluated conditions used as columns in the schema. They are not stored — they are determined at authorization time.

3.1 Self

  • Matches when: The actor is targeting their own identity (event.from == content.target).
  • Use case: AC events — leaving a group (Move Self
    ), stepping down from a trait (Revoke Self
    ).

3.2 Sender

  • Matches when: The actor authored the referenced event (event.from == original_event.from).
  • Use case: Content events — editing/deleting own messages (message Sender
    ), removing own reactions (reaction Sender
    ), updating own KV slot (Own Sender
    ).

3.3 Public

  • Matches when: Always. Any identity, any State, including OUTSIDER.
  • Use case: Public read access to group messages, public enclave manifests.

4. Authorization Algorithm

The authorization algorithm determines whether an identity can perform a specific operation on a specific event type.

isAuthorized(identity_bitmask, event_type, operation, is_self, is_sender) → bool
 
1. Extract State from bitmask (bits 0-7).
 
2. Collect allowed ops:
   a. State ops (from State column for identity's current State)
   b. trait positive ops (OR across all held trait flags)
   c. Self ops (if is_self == true: event.from == content.target)
   d. Sender ops (if is_sender == true: event.from == original_event.from)
   e. Public ops (always applies)
 
3. Collect denied ops:
   negative ops from all sources (State _X, trait _X, Self _X, Sender _X, Public _X)
 
4. Compute effective:
   effective = allowed − denied
 
5. Return: operation ∈ effective

No State gate. traits can extend any State with new capabilities. Safety against invalid State+trait combinations comes from operational constraints:

  • Move clears all traits
  • Grant validates target State (schema-defined)

4.1 Deny Override

Negative ops always win over positive ops regardless of source. If the effective set has both C (from State) and _C (from a trait, State, or Context), the result is: C is removed.


5. Manifest Format

5.1 Manifest Sections

A manifest declares all RBAC rules for an enclave. Ten sections:

SectionPurposeEntry format
statesApp-defined StatesArray of UPPER_CASE names. Assigned enum values 1+ sequentially.
traitsApp-defined traitsArray of name(rank) strings. Assigned bit positions 8+ sequentially. Lower rank = higher authority.
readersRead access[{ type, reads }]. Each entry declares R ops for one column.
initInitial identities{ identity, state, traits[] }. Bootstrap SMT at enclave creation.
movesState transitions{ event, from, to, operator, ops }. Optional alias and gate.
grantstrait assignment rules{ event (Grant/Revoke), operator[], scope[], trait[] }
transfersAtomic trait movement{ trait, scope[] }. Operator must hold the trait.
slotsKV state (Shared/Own){ event (Shared/Own), operator, ops, key }
lifecycleEnclave lifecycle{ event (Pause/Resume/Migrate/Terminate), operator, ops }
customsApp-defined content events{ event, operator, ops }

5.2 Section Details

readers — Column-oriented read declarations. Each entry is { type, reads }:

  • type: A declared State, trait, or Context.
  • reads: Array of event type names this column gets R on, or "*" for all events.

moves — Each entry authorizes one State transition for one operator.

grants — Each entry authorizes Grant or Revoke of specific traits, scoped to specific States.

  • operator: Who can grant/revoke (array — Contexts, traits, or States).
  • scope: Which States the target must be in (array).
  • trait: Which traits can be granted/revoked (array).

transfers — Each entry authorizes atomic movement of a trait from one identity to another.

  • trait: Which trait can be transferred.
  • scope: Which States the target must be in (array).
  • The operator is implicit: must hold the trait being transferred.

slots — KV state entries. Each entry requires a key field (lowercase, app-defined).

  • Shared: Enclave-wide singleton per key. Last write wins.
  • Own: Per-identity slot per key. Node enforces per-identity isolation.
  • Protocol-reserved keys: gate:*, lifecycle. Apps cannot write to reserved keys.

lifecycle — Protocol-defined enclave lifecycle events. State stored in Shared("lifecycle").

customs — App-defined content events (lowercase names). Standard { event, operator, ops } entries.

5.2.1 Naming Convention

  • Protocol events: PascalCase (Move, Grant, Shared, Pause, etc.)
  • App events: lowercase (message, request, sent, etc.)
  • States: UPPER_CASE (MEMBER, PENDING, BLOCKED, etc.)
  • traits: lower_case (admin, muted, etc.)
  • Contexts: PascalCase (Self, Sender, Public)
  • KV keys: lowercase (topic, profile, etc.)

5.2.2 Common Entry Properties

Any entry in any section may include:

  • alias: Optional. Names the entry for referencing (Gate keys, UI, logs, API).
  • gate: Optional. { operator[] } — declares who can toggle the gate. Requires alias. Gate state stored in Shared("gate:<alias>"). When gate is closed, the event is rejected before RBAC runs.
  • preserve: Optional (moves only). If true, trait flags are kept after the Move. Default false (clear all traits).

6. Validation Rules

  1. In and Out — Every State in states must appear as to in at least one moves entry or be assigned in at least one init entry (in). Any State with no ops must also appear as from in at least one moves entry (out).
  2. No Stuck Traits — Every trait in traits must have at least one assign path (Grant entry or Transfer entry) and at least one remove path (Revoke entry or Transfer entry). Traits only assigned in init are exempt from the assign path requirement.
  3. Valid Operators — Every operator must be a declared State, declared trait, or a Context (Self, Sender, Public).
  4. Read/Write Completeness — Every event must have at least one operator with C ops and at least one operator with R ops.
  5. Reserved Keys — App-defined slots keys must not use reserved prefixes (gate:, lifecycle).
  6. Gate Requires Alias — If an entry has gate, it must have alias.
  7. Valid Ranks — Every trait must declare a rank via name(N) syntax. All rank values must be non-negative integers.
  8. Complete States — Every State name referenced in the manifest (moves from/to, grants scope, transfers scope, init state) must be either declared in states or be OUTSIDER.

7. Processing Pipeline

The full event processing pipeline, from submission to commitment:

1. Signature verification (is the event signed by the claimed identity?)
2. Duplicate check (has this event already been committed?)
3. Lifecycle check (is the enclave active, not paused/terminated?)
4. Gate check (is this event type/transition currently enabled?)
5. RBAC authorization (Section 4 algorithm)
6. Rank check (for AC events targeting another identity — see Rank Rule below)
7. Content validation (event-type-specific format checks)
8. State mutation (apply bitmask changes for AC events)
9. SMT update (write new bitmask, compute new root)
10. CT append (add event to the certificate transparency log)
11. Commit (finalize sequence number, return receipt)

Rank Rule. For any AC event (Move, Grant, Revoke) targeting another identity: if the operator holds any trait, and the target holds any trait, the operator's best rank (lowest rank number among held traits) must be strictly less than the target's best rank. If either party holds no traits, or the operator is Self-targeting, the rank check is skipped. Violation → reject (RANK_INSUFFICIENT).


8. Event Processing

When reading a target's bitmask from SMT, if no leaf exists, treat the bitmask as 0x00 (OUTSIDER, no traits).

8.1 Move

Changes an identity's State. Clears all traits. The core lifecycle operation.

Event content:

{ "target": "<target_pub>", "from": "PENDING", "to": "MEMBER", "preserve": true }

The preserve field is a manifest entry selector, not a behavioral directive. The node matches the content's from, to, and preserve against the moves entries to find the authorizing entry. Two entries with the same from→to but different preserve values are distinct Moves.

Processing:

1. Resolve from/to State names to enum values.
2. Match against moves entries: find the entry where from, to, and preserve
   all match. If no match → reject (UNAUTHORIZED).
3. Read target's current bitmask from SMT.
4. Verify: current State enum == from value.
   If mismatch → reject (STATE_MISMATCH).
5. Compute new bitmask:
   a. Set State enum (bits 0-7) to the "to" value.
   b. If the matched moves entry has preserve: true → keep all trait flags.
      Otherwise (default): clear all trait flags.
6. If entire bitmask == 0: delete SMT leaf.
   Else: write new bitmask to SMT.

Authorization: Move events are authorized via the moves section of the manifest. Each entry specifies from, to, operator, ops, and optionally preserve. The content's from, to, and preserve fields together select the matching entry.

Move matching: State is an enum. The check is exact: current_state == from. trait bits are ignored. No CAS on the full bitmask — only the State enum is compared.

Application payload: Move content may carry additional application-defined fields alongside the required target, from, and to fields. The node does not interpret these fields — they are opaque payload for the application layer. Example: DM enclaves carry epoch key-establishment data in Move events (see app/dm.md).

8.2 Grant

Adds a trait flag to an identity's bitmask.

Event content:

{ "target": "<target_pub>", "trait": "admin" }

If the trait has P (Push) ops in the schema, the event includes an endpoint:

{ "target": "<target_pub>", "trait": "dataview", "endpoint": "https://..." }

Processing:

1. Resolve trait name to bit position (from manifest traits array).
2. Read target's current bitmask from SMT.
3. Verify: target's current State is in the allowed scope.
   If not → reject (INVALID_STATE_FOR_GRANT).
4. Rank check (see §7 Rank Rule).
5. Set the trait bit: new_bitmask = current | (1 << trait_bit).
6. If trait has P ops in schema AND endpoint is provided: register push endpoint (operational state, not in SMT).
7. Write new bitmask to SMT (create leaf if it doesn't exist).

Authorization: Grant events are authorized via Grant schema entries. Each entry specifies operator (who can grant), scope (what States the target must be in), and trait (what traits can be granted).

No separate Grant_Push event type. The node detects P ops from the schema and handles endpoint registration as part of regular Grant.

8.3 Revoke

Removes a trait flag from an identity's bitmask.

Event content:

{ "target": "<target_pub>", "trait": "admin" }

Processing:

1. Resolve trait name to bit position.
2. Read target's current bitmask from SMT.
3. Rank check (see §7 Rank Rule).
4. Clear the trait bit: new_bitmask = current & ~(1 << trait_bit).
5. Write new bitmask to SMT.

Revoking a trait the target doesn't have is a no-op (bit was already 0).

Self is allowed as operator for Revoke (voluntarily drop own trait).

8.4 Transfer

Atomically moves a trait from one identity to another. The operator must hold the trait being transferred.

Event content:

{ "target": "<target_pub>", "trait": "owner" }

Processing:

1. Verify operator holds the trait being transferred.
2. Verify target is not the operator (no self-transfer).
   If same → reject (INVALID_TRANSFER_TARGET).
3. Read target's current bitmask from SMT.
4. Verify target does not already hold the trait.
   If already held → reject (TRAIT_ALREADY_HELD).
5. Verify: target's current State is in the allowed scope.
   If not → reject (INVALID_STATE_FOR_TRANSFER).
6. Clear trait bit on operator's bitmask.
7. Set trait bit on target's bitmask.
8. Write both bitmasks to SMT.

Authorization: Transfer events are authorized via the transfers section. Each entry specifies trait (which trait can be transferred) and scope (which States the target must be in).

8.5 Gate

Pre-RBAC check that enables/disables specific events at the enclave level. Gates are open by default.

Event content:

{ "gate": "applications", "open": false }

Processing:

1. Verify submitter is an allowed operator for this gate (from manifest).
2. Update gate state in KV Shared: Shared("gate:<id>") = open value.

Gate state is checked BEFORE the RBAC authorization algorithm. If a gated event type/transition is closed, the event is rejected before RBAC runs.

8.6 AC_Bundle

Atomic multi-event bundle. All events succeed or none apply.

Event content:

{
  "events": [
    { "event": "Move", "target": "<pub>", "from": "PENDING", "to": "MEMBER" },
    { "event": "Grant", "target": "<pub>", "trait": "admin" },
    { "event": "Grant", "target": "<pub>", "trait": "moderator" }
  ]
}

Processing:

1. For each event in order:
   a. Validate against simulated intermediate state.
   b. Apply to simulated state.
   If any event fails → reject entire bundle.
2. If all pass: apply all changes atomically to SMT.

8.7 Lifecycle Events

Lifecycle events control enclave-level state. They store their state in KV Shared (SMT namespace 0x02) with the reserved lifecycle key.

Pause

Stops the enclave from accepting new events (except Resume).

Event content:
{ "event": "Pause" }
Processing:
1. Verify current lifecycle state is "active".
   If not → reject (INVALID_LIFECYCLE_STATE).
2. Update KV Shared: Shared("lifecycle") = "paused".

Resume

Lifts a pause, restoring normal operation.

Event content:
{ "event": "Resume" }
Processing:
1. Verify current lifecycle state is "paused".
   If not → reject (INVALID_LIFECYCLE_STATE).
2. Update KV Shared: Shared("lifecycle") = "active".

Migrate

Initiates migration of the enclave to a new node.

Event content:
{ "event": "Migrate", "target_node": "<node_pub>" }
Processing:
1. Verify current lifecycle state is "active".
   If not → reject (INVALID_LIFECYCLE_STATE).
2. Update KV Shared: Shared("lifecycle") = "migrating".

Terminate

Permanently shuts down the enclave. Irreversible.

Event content:
{ "event": "Terminate" }
Processing:
1. Verify current lifecycle state is not "terminated".
   If terminated → reject (INVALID_LIFECYCLE_STATE).
2. Update KV Shared: Shared("lifecycle") = "terminated".

Lifecycle Check

Lifecycle state is checked at step 3 of the processing pipeline (Section 7), before Gate and RBAC checks:

Lifecycle check → Gate check → RBAC authorization
StateAccepts events?
activeAll events
pausedOnly Resume (from owner)
migratingNode-managed (implementation-defined)
terminatedNo events

8.8 KV Events (Shared / Own)

Mutable key-value state stored in SMT namespace 0x02. KV events are NOT content events — they form their own category. Authorization uses the same schema matrix as all other events.

8.8.1 Problem

Content events are append-only (the CT is a log). To represent "current topic" or "my display name", apps would need to scan the CT for the latest event. KV events provide first-class mutable state with SMT inclusion proofs for the current value.

8.8.2 Shared

Enclave-wide singleton. One slot per key per enclave. Last write wins.

Event content:
{ "key": "topic", "value": "General Discussion" }

SMT key: H(0x02 || key_name) SMT value: H(content)

Any actor with C on Shared (for matching key) can overwrite. History preserved in CT.

8.8.3 Own

Per-identity slot. One slot per key per identity. Each identity can only write to their own slot.

Event content:
{ "key": "profile", "value": { "display_name": "Alice", "status": "Available" } }

SMT key: H(0x02 || key_name || commit.from) SMT value: H(content)

Per-identity isolation enforced by the node — the from in the SMT key is always commit.from. No identity field in content needed.

8.8.4 Schema Representation

KV events use the standard schema format with an additional key field:

{ "event": "Shared", "operator": "admin", "ops": ["C"], "key": "topic" }
{ "event": "Shared", "operator": "MEMBER", "ops": ["R"], "key": "topic" }
{ "event": "Own", "operator": "MEMBER", "ops": ["C"], "key": "profile" }
{ "event": "Own", "operator": "Sender", "ops": ["U"], "key": "profile" }

The key field constrains which KV slot the operator can access.

Operations for KV:
OpMeaning
CCreate or overwrite the value
RRead the current value
UUpdate an existing value
DClear the value (remove SMT leaf)
PPush delivery on value change
NNotify on value change

8.8.5 Delete (D)

  • Shared: Remove SMT leaf at H(0x02 || key). Value cleared.
  • Own: Remove SMT leaf at H(0x02 || key || commit.from). Only your own slot.

8.8.6 Protocol-Reserved KV Keys

Gate state (Section 8.5) is stored as KV Shared with the gate: key prefix:

Shared("gate:applications") = true/false

Gate and lifecycle KV slots are protocol-managed — written by Gate and lifecycle events, not arbitrary Shared writes. Reserved keys: gate:* prefix and lifecycle.

8.8.7 Bounding

No dynamic keys. Only keys declared in schema entries can be written:

  • Shared: bounded by manifest (only declared keys)
  • Own: bounded by RBAC (one slot per key per identity, identities controlled by Move)

8.8.8 KV vs Content Events

AspectContent EventKV Event
HistoryFull history mattersOnly current value matters
SMTNo SMT entrySMT leaf tracks current hash
ProofsCT inclusion onlySMT inclusion proof for current state
Use caseMessages, reactionsTopic, settings, profiles, status

Both live in the same CT. KV events maintain a "current state" view in SMT 0x02.

8.8.9 Read API

  • Shared: GET /enclave/{id}/kv/{key}
  • Own: GET /enclave/{id}/kv/{key}/{identity}

The SMT provides inclusion proofs for the current value hash.


9. Example Manifest

This is the Group Chat enclave manifest from group.md. For other examples, see personal.md and dm.md.

{
  "states": ["PENDING", "MEMBER", "BLOCKED"],
  "traits": ["owner(0)", "admin(1)", "muted(2)", "dataview(3)"],
  "readers": [
    { "type": "MEMBER", "reads": "*" }
  ],
 
  "moves": [
    { "event": "Move", "from": "OUTSIDER", "to": "PENDING", "operator": "Self", "ops": ["C"],
      "alias": "applications", "gate": { "operator": ["owner", "admin"] } },
    { "event": "Move", "from": "OUTSIDER", "to": "MEMBER", "operator": "Self", "ops": ["C"],
      "alias": "auto_join", "gate": { "operator": ["owner"] } },
    { "event": "Move", "from": "OUTSIDER", "to": "MEMBER", "operator": "admin", "ops": ["C"] },
    { "event": "Move", "from": "OUTSIDER", "to": "BLOCKED", "operator": "admin", "ops": ["C"] },
    { "event": "Move", "from": "PENDING", "to": "MEMBER", "operator": "admin", "ops": ["C"] },
    { "event": "Move", "from": "PENDING", "to": "OUTSIDER", "operator": "admin", "ops": ["C"] },
    { "event": "Move", "from": "MEMBER", "to": "OUTSIDER", "operator": "Self", "ops": ["C"] },
    { "event": "Move", "from": "MEMBER", "to": "OUTSIDER", "operator": "admin", "ops": ["C"] },
    { "event": "Move", "from": "MEMBER", "to": "BLOCKED", "operator": "admin", "ops": ["C"] },
    { "event": "Move", "from": "BLOCKED", "to": "OUTSIDER", "operator": "admin", "ops": ["C"] }
  ],
 
  "grants": [
    { "event": "Grant", "operator": ["admin"], "scope": ["MEMBER"], "trait": ["muted"] },
    { "event": "Grant", "operator": ["owner"], "scope": ["MEMBER"], "trait": ["admin"] },
    { "event": "Grant", "operator": ["owner"], "scope": ["OUTSIDER", "MEMBER"], "trait": ["dataview"] },
    { "event": "Revoke", "operator": ["admin"], "scope": ["MEMBER"], "trait": ["muted"] },
    { "event": "Revoke", "operator": ["owner"], "scope": ["MEMBER"], "trait": ["admin"] },
    { "event": "Revoke", "operator": ["owner"], "scope": ["OUTSIDER", "MEMBER"], "trait": ["dataview"] },
    { "event": "Revoke", "operator": ["Self"], "scope": ["MEMBER"], "trait": ["admin"] }
  ],
 
  "transfers": [
    { "trait": "owner", "scope": ["MEMBER"] }
  ],
 
  "slots": [
    { "event": "Shared", "operator": "admin", "ops": ["C", "U"], "key": "topic" },
    { "event": "Shared", "operator": "dataview", "ops": ["P"], "key": "topic" },
 
    { "event": "Own", "operator": "MEMBER", "ops": ["C"], "key": "profile" },
    { "event": "Own", "operator": "Sender", "ops": ["U"], "key": "profile" }
  ],
 
  "lifecycle": [
    { "event": "Pause", "operator": "owner", "ops": ["C"] },
    { "event": "Resume", "operator": "owner", "ops": ["C"] },
    { "event": "Migrate", "operator": "owner", "ops": ["C"] },
    { "event": "Terminate", "operator": "owner", "ops": ["C"] }
  ],
 
  "customs": [
    { "event": "message", "operator": "MEMBER", "ops": ["C"] },
    { "event": "message", "operator": "admin", "ops": ["D"] },
    { "event": "message", "operator": "muted", "ops": ["_C", "_U"] },
    { "event": "message", "operator": "dataview", "ops": ["P"] },
    { "event": "message", "operator": "Sender", "ops": ["U", "D"] },
    { "event": "message", "operator": "BLOCKED", "ops": ["_U", "_D"] },
 
    { "event": "reaction", "operator": "MEMBER", "ops": ["C"] },
    { "event": "reaction", "operator": "Sender", "ops": ["D"] },
    { "event": "reaction", "operator": "muted", "ops": ["_C"] },
    { "event": "reaction", "operator": "BLOCKED", "ops": ["_D"] },
 
    { "event": "notice", "operator": "admin", "ops": ["C", "D"] },
 
    { "event": "rotate", "operator": "admin", "ops": ["C"] }
  ],
 
  "init": [
    { "identity": "<owner_pub>", "state": "MEMBER", "traits": ["owner", "admin"] }
  ]
}

Event-Operator Matrix

EventMEMBEROUTSIDERPENDINGBLOCKEDowner(0)admin(1)muted(2)dataview(3)SelfSender
messageCR_U_DD_C_UPUD
reactionCR_D_CD
noticeRCD
rotateRC
Shared(topic)RCUP
Own(profile)CRU
Move(OUTSIDER, PENDING)RC
Gate(applications)RCC
Move(OUTSIDER, MEMBER)RCC
Gate(auto_join)RC
Move(OUTSIDER, BLOCKED)RC
Move(PENDING, MEMBER)RC
Move(PENDING, OUTSIDER)RC
Move(MEMBER, OUTSIDER)RCC
Move(MEMBER, BLOCKED)RC
Move(BLOCKED, OUTSIDER)RC
Grant(muted)RC
Grant(admin)RC
Grant(dataview)RC
Revoke(muted)RC
Revoke(admin)RCC
Revoke(dataview)RC
Transfer(owner)RC
PauseRC
ResumeRC
MigrateRC
TerminateRC

Columns: States (MEMBER, OUTSIDER, PENDING, BLOCKED) → traits (owner, admin, muted, dataview) → Contexts (Self, Sender).


10. SMT Namespace Table

NamespacePurposeKeyValue
0x00RBACH(identity)Bitmask (State + traits)
0x01EventStatusH(event_hash)Status flags (Update/Delete)
0x02KV StateH(key) or H(key || identity)H(content)

Gate state lives in 0x02 with the gate: key prefix. Lifecycle state lives in 0x02 with the lifecycle key.


11. Appendix: Removed Events

Four event types from the v1 spec are removed. Each is replaced by a more general mechanism already in the registry.

RemovedReplacementRationale
Move_SelfMove with operator: "Self"Self is a Context, not an event type. The node enforces event.from == content.target when the moves entry has operator: "Self". No expressiveness lost.
Revoke_SelfRevoke with operator: "Self"Same pattern. Self-targeting is an operator constraint, not a separate event type.
Grant_PushGrantThe node detects P (Push) ops from the schema for the granted trait. If the trait has P ops and the Grant content includes an endpoint field, the node registers the push endpoint. No separate event type needed.
Force_Move(removed, no replacement)Owner can define all necessary transitions in moves. State is an 8-bit enum (not a combinatorial bitmask), so Owner can always move anyone to any State by having the right moves entries. Force_Move added no capability that moves entries cannot express.