ENC Protocol Specification
1. Overview
Protocol Summary
ENC (encode, encrypt, enclave) is a protocol for building log-based, verifiable, sovereign data structures with Role-Based Access Control (RBAC).
Key properties:
- Append-only event log — immutable sequence of finalized events
- Verifiable state — Sparse Merkle Tree (SMT) for RBAC and event status
- Cryptographic proofs — Certificate Transparency (CT) for log integrity
- Single sequencer — one node orders and finalizes events per enclave
Trust Model
The ENC protocol defines two query paths with different trust properties:
Enclave (Source of Truth)
- Queries to the enclave node return verifiable proofs
- Clients can verify RBAC state, event existence, and event status (U/D) against the SMT root
- The enclave is the final arbiter when disputes arise
DataView (Convenience)
A DataView is a separate service that indexes, aggregates, and transforms enclave data for efficient querying.
- Clients trust DataView responses without cryptographic verification
- DataView is optimized for performance and flexibility, not provability
- If a client suspects incorrect data, they SHOULD verify against the enclave directly
A DataView can receive enclave data through three methods, each requiring a role assignment:
| Method | Permission | Description |
|---|---|---|
| Query | R (Read) | DataView queries the enclave directly as a member |
| Push | P | Node delivers full events to DataView proactively |
| Notify | N | Node delivers lightweight notifications; DataView fetches as needed |
Recommendation: Applications SHOULD use DataView for routine queries and reserve direct enclave queries for:
- Dispute resolution
- High-value transactions
- Audit trails
2. Cryptographic Primitives
Signature Schemes
The ENC protocol supports multiple signature schemes over the secp256k1 curve. The scheme is declared by the alg field on commits and events.
Supported Algorithms
alg | Scheme | Specification | Signature Format | Note |
|---|---|---|---|---|
"schnorr" | Schnorr | BIP-340 | 64 bytes (R‖s) | Default |
"ecdsa" | ECDSA | SEC 1 v2 §4.1 + RFC 6979 | 64 bytes (r‖s compact) | For NFC/hardware signers |
When the alg field is absent, "schnorr" is assumed. Nodes MUST reject commits where alg is present but not one of the supported values. Nodes MUST NOT skip signature verification for any alg value.
Identity Key
Identity keys (id_pub) are always 32-byte x-only secp256k1 public keys (BIP-340 format), regardless of which signature algorithm is used. This is the canonical identity.
To verify an ECDSA signature against an identity key, the node derives the compressed ECDSA public key: 0x02 || id_pub. This works because BIP-340 x-only keys always have even y-coordinate, so the compressed prefix is always 0x02.
Schnorr (BIP-340)
- All
schnorr(message, private_key)operations MUST use the secp256k1 elliptic curve. - Signatures MUST conform to BIP-340 (32-byte x-only public keys, 64-byte signatures).
All Schnorr signatures MUST be deterministic. Implementations MUST use the default nonce derivation specified in BIP-340, which derives the nonce from:
- The private key
- The message being signed
- Auxiliary randomness MUST be set to 32 zero bytes. Implementations MUST NOT use random auxiliary data.
ECDSA (secp256k1)
- All
ecdsa(message, private_key)operations MUST use the secp256k1 elliptic curve. - Signatures MUST use compact encoding: raw
r || s(32 bytes each, big-endian, total 64 bytes). NOT DER encoding. - Both
randsMUST be in the range[1, n-1]wherenis the secp256k1 curve order. - Signatures MUST use low-s normalization (s ≤ n/2) per BIP-62 / BIP-146.
- Signing MUST be deterministic per RFC 6979.
- The
fromfield still contains the BIP-340 x-only public key (32 bytes), not the ECDSA compressed key. - Implementations MUST use the BIP-340-adjusted private key (negated if the public point has odd y) for ECDSA signing, ensuring consistency with the
0x02 || id_pubderivation.
Determinism
Both schemes produce deterministic signatures: the same message signed with the same key always produces the same signature. This ensures event IDs are deterministic and verifiable.
Verification
Verifiers MUST check the alg field and use the corresponding algorithm. Verifiers MUST NOT attempt multiple algorithms as a fallback. The verification path is always deterministic:
"schnorr"(or absent):schnorr_verify(hash, sig, from)"ecdsa":ecdsa_verify(hash, sig, 0x02 || from)
Sequencer and Server Signatures
The alg field applies only to client signatures (sig). Sequencer signatures (seq_sig), STH signatures, and session token signatures always use Schnorr (BIP-340). Sequencers are server nodes without hardware signing constraints.
Security Considerations
Using the same secp256k1 private key for both Schnorr and ECDSA is safe under the following conditions, which this spec enforces:
- Deterministic nonce generation — BIP-340 and RFC 6979 derive nonces from different domain-separated functions. The same
(key, message)pair produces different nonces in each scheme, preventing nonce reuse across algorithms. - No cross-algorithm forgery — Schnorr and ECDSA have structurally different verification equations. A valid signature under one scheme cannot be reinterpreted as valid under the other.
algfield integrity — Ifalgis tampered in transit, signature verification fails. This is a denial-of-service (commit rejected), not a forgery.
Hash Function
- All
sha256()operations use SHA-256 as defined in FIPS 180-4.
Hash Encoding
All hash pre-images MUST be serialized using CBOR (RFC 8949) with deterministic encoding before hashing.
Canonical Hash Function:This specification defines:
H(fields...) = sha256(cbor_encode([fields...]))All hash formulas in this document use H() implicitly. When you see:
sha256([0x10, enclave, from, type, content_hash, exp, tags])it means:
sha256(cbor_encode([0x10, enclave, from, type, content_hash, exp, tags]))- Implementations MUST use deterministic CBOR encoding (RFC 8949 Section 4.2)
- Integer prefixes (0x00, 0x10, etc.) are encoded as CBOR unsigned integers
- Binary data (hashes, public keys) are encoded as CBOR byte strings
- Strings (content, type) are encoded as CBOR text strings (UTF-8)
- Empty string
""is encoded as CBOR text string with length 0 (0x60) - Arrays are encoded as CBOR arrays with definite length
Hash Prefix Registry
To prevent hash collisions between different data types, all hash operations use a unique single-byte prefix:
| Prefix | Purpose |
|---|---|
0x00 | CT leaf (RFC 9162) |
0x01 | CT internal node (RFC 9162) |
0x10 | Commit hash |
0x11 | Event hash |
0x12 | Enclave ID |
0x20 | SMT leaf |
0x21 | SMT internal node |
Prefixes 0x02–0x0F, 0x13–0x1F, and 0x22–0x2F are reserved for future use.
Signature contexts use string prefixes (not byte prefixes) for domain separation:
| Context | Prefix String |
|---|---|
| STH signature | "enc:sth:" |
| Session token | "enc:session:" |
String prefixes are concatenated with data before hashing: sha256(prefix || data).
Wire Format
Transport Encoding:The normative wire format for v1 is JSON. CBOR is used only for hash computation, not transport.
JSON Encoding:When serializing for transport or storage as JSON:
| Type | Encoding | Example |
|---|---|---|
| Hash (32 bytes) | Hex string, no prefix | "a1b2c3..." (64 chars) |
| Public key (32 bytes) | Hex string, no prefix | "a1b2c3..." (64 chars) |
| Signature (64 bytes) | Hex string, no prefix | "a1b2c3..." (128 chars) |
| Role bitmask | Hex string with 0x prefix | "0x100000002" |
| Content | UTF-8 string | "hello world" |
| Binary in content | Base64 encoded | "SGVsbG8=" |
| Integers | JSON number | 1706000000 |
Consistency: All bitmask values (State + trait flags) in JSON (event content, proofs, API responses) MUST use the 0x hex prefix format.
When serializing for hashing or binary transport:
| Type | Encoding |
|---|---|
| Hash, public key, signature | CBOR byte string (major type 2) |
| Role bitmask | CBOR unsigned integer (major type 0) |
| Content, type | CBOR text string (major type 3) |
3. Core Concepts
Enclave
An Enclave is a log-based, verifiable, sovereign data structure with Role-Based Access Control (RBAC).
An enclave maintains:
- An append-only event log — immutable sequence of finalized events
- A verifiable state (SMT) — unified tree storing RBAC state and event status
- A structural proof — append-only Merkle tree proving log integrity (see Structural Proof)
Node
A Node is a host for one or more enclaves.
A node is responsible for:
- Storing data for enclave
- Serving queries
- Producing cryptographic proofs
- Sequencing commits into events (if acting as sequencer)
Sequencer Model
Each enclave has a single sequencer — the node responsible for ordering and finalizing commits.
Assignment:- The node that accepts and finalizes the Manifest event becomes the sequencer for that enclave
- The sequencer's identity key is recorded in the Manifest event's
sequencerfield - The sequencer is discoverable via Registry (
Reg_Enclave)
- Accept valid commits and assign
seq,timestamp - Sign events with
seq_sig - Maintain the append-only Merkle tree
- Maintain the Enclave State SMT
- Deliver P (Push) and N (Notify) to registered recipients
- See Migration section for sequencer handoff protocol
Note: Multi-sequencer (consensus-based) models are out of scope for v1.
Client
A Client is a software agent that controls an Identity Key by holding (or having authorized access to) the corresponding private key id_priv, and can produce signatures or decrypt/encrypt on behalf of the identity id_pub according to the ENC protocol.
Notes:
- A client may be an app, service, or device component.
id_privmay be held directly or accessed via a secure signer (e.g., OS keystore, HSM, enclave, wallet).
Identity Key
An Identity Key is a 256-bit public key (id_pub) used to represent an identity in the ENC protocol.
4. Event Lifecycle
Commit
A Commit is a client-generated, signed message proposing an event.
A commit:
- Is authored by a client
- Is cryptographically bound to its content and metadata
- Has not yet been ordered or finalized by a node
Commit Structure
{
hash: <hex64>,
enclave: <enclave id>,
from: <sender_id_pub>,
type: <string>,
content_hash: <hex64>,
content: <string>,
exp: <unix timestamp>,
tags: [ <name | str1 | str2>, ... ],
alg: <string>,
sig: <signature over hash>
}Where:
- hash: commitment hash of the commit fields
- enclave: destination enclave
- from: identity key of the author
- type: commit type, defined within the enclave scope
- content: payload (UTF-8 string; MAY be empty
""for events with no payload like Pause/Resume; binary data SHOULD be base64 encoded; large binaries SHOULD use external resource URLs) - exp: latest time at which the node may accept the commit (Unix epoch milliseconds; also acts as a nonce)
- tags: node-level metadata (see Tags)
- alg: signature algorithm used for
sig. One of"schnorr"(default) or"ecdsa". MAY be omitted if"schnorr". - sig: signature by
from, proving authorship
Commit Hash Construction
This formula applies to ALL event types (Manifest, Grant, Chat_Message, etc.):
_content_hash = sha256(utf8_bytes(content))
hash = H(0x10, enclave, from, type, _content_hash, exp, tags)
sig = sign(hash, from_priv, alg)Where sign() dispatches on alg:
"schnorr"(or absent):schnorr(hash, from_priv)per BIP-340"ecdsa":ecdsa(hash, from_priv)per RFC 6979, compact r‖s encoding
Note: alg is not included in the hash formula. The hash identifies the content commitment; the signature algorithm is how that commitment is proven.
Where utf8_bytes() returns the raw UTF-8 byte sequence. No Unicode normalization (NFC/NFD) is performed at the protocol level. The content structure varies by event type, but the hash formula is identical.
Note: _content_hash uses raw SHA-256 on bytes, not H(), because content is already a byte string.
If content contains binary data (e.g., images, files), the data MUST be base64-encoded before inclusion. The content_hash is computed over the base64-encoded string, not the decoded binary bytes.
Example:
- Binary data:
0x48656c6c6f(5 bytes, "Hello") - Base64 encoded in content:
"SGVsbG8=" content_hash = sha256(utf8_bytes("SGVsbG8="))— hash of 8 UTF-8 bytes
This ensures the hash matches what is transmitted and stored. Implementations MUST NOT decode base64 before hashing.
This establishes:
- Integrity of content
- Binding between author and intent
Note: content_hash is computed for the commit hash but is not stored in the finalized event. The event stores only content; recipients can recompute the hash if needed for verification.
Wire Format: The content_hash field is NOT transmitted. The client sends content; the node computes content_hash = sha256(utf8_bytes(content)) for commit hash verification.
The node MUST store and serve event.content exactly as received in the commit — byte-for-byte, with no normalization or transformation. Any modification would invalidate the commit hash.
If applications treat content as structured data (e.g., JSON), they MUST define their own canonical encoding rules to ensure cross-client byte-identical hashing. The protocol treats content as an opaque byte string.
Tags
Tags provide node-level metadata that instructs the node how to process an event.
Key distinction:- Content — opaque to the node; the node stores and serves it without interpretation
- Tags — understood by the node; predefined tags trigger specific node behaviors
Unlike protocols where tags serve as query indexes, ENC data is already scoped to enclaves. Tags exist primarily to convey processing instructions to the node.
Structure:Tags are an array of arrays. Each tag has:
- Position 0: tag name (key)
- Position 1: primary value
- Position 2+: additional values (optional)
All tag values MUST be strings. Numeric or other typed values are encoded as their string representation.
[
["<name>", "<value>", "<additional>", ...],
...
]Hash Ordering: When computing the commit hash, tags are hashed as a CBOR array in their original order. The order of tags in the array is significant for hash computation.
Empty Tags: An empty tags array [] is valid.
[
["auto-delete", "1706000000000"],
["r", "abc123...", "reply"]
]| Tag | Description |
|---|---|
r | Reference — references another event. Format: ["r", "<event_id>", "<context>"]. See context values below. |
auto-delete | Auto-delete timestamp — node SHOULD delete the event content after this Unix timestamp (milliseconds, same as exp and timestamp) |
r Tag Context Values:
| Context | Meaning |
|---|---|
| (omitted) | General reference (default) |
"target" | Target for Update/Delete operations |
"reply" | Reply to referenced event |
"quote" | Quote/repost of referenced event |
"thread" | Part of a thread starting at referenced event |
Custom context values are allowed for application-specific semantics. Nodes treat unrecognized contexts as general references.
Note: The exp field (commit acceptance window) and auto-delete tag (event retention) serve different purposes. exp controls when a commit can be accepted; auto-delete controls when the finalized event should be removed from storage.
| Aspect | Auto-delete (tag) | Delete (event) |
|---|---|---|
| Mechanism | Node silently removes content after timestamp | Explicit Delete event updates SMT |
| Verifiable | No — trust-based | Yes — auditable proof |
| SMT state | Unchanged (remains "active") | Updated to "deleted" |
| Audit trail | No | Yes (who, when, why) |
| Use case | Ephemeral content, disappearing messages | Compliance, moderation, user-initiated removal |
Important: Auto-delete is trust-based. Clients trust the node to honor the timestamp. A malicious node could delete content early without detection. For verifiable deletion with audit trail, use Delete events instead.
Relationship with exp:The auto-delete timestamp MUST be greater than exp. The commit must be accepted before auto-delete takes effect. Nodes SHOULD reject commits where auto-delete <= exp as semantically invalid.
Auto-delete does NOT update the SMT. The event remains "active" in SMT state (no entry in Event Status namespace). Auto-delete only affects storage — the node removes content but event metadata may remain. This is intentional: auto-delete is trust-based, not verifiable via SMT proofs.
Additional predefined tags may be defined by the protocol or application schemas.
Event Finalization
Upon receiving a valid commit, a node performs:
- Expiration check — reject if
exp< current time - Deduplication check — reject if commit
hashwas already processed (see Replay Protection) - RBAC authorization check — verify sender has C permission for this event type
- Sequencing and timestamp assignment
If accepted, the node finalizes the commit into an event by adding node-generated fields.
Replay Protection
The node MUST reject commits with a hash that has already been processed for this enclave.
- Node maintains a set of accepted commit hashes per enclave
- Before accepting a commit, check if
hashexists in the set - If duplicate, reject the commit
- Only add hash to set AFTER successful acceptance
- Hashes MAY be garbage collected after
exp + 60000ms (60 seconds buffer for clock skew)
Note: Rejected commits are NOT added to the deduplication set. A commit that was rejected for authorization failure can be resubmitted (e.g., after the sender is granted the required role).
This prevents replay attacks where an attacker resubmits a valid commit multiple times within the expiration window.
Expiration Window Limit:Nodes MUST reject commits where the expiration is too far in the future:
exp - current_time > MAX_EXP_WINDOW → rejectThe protocol defines MAX_EXP_WINDOW = 3600000 (1 hour in milliseconds). Implementations MAY use a shorter window.
This prevents storage DoS attacks where clients submit commits with extremely large exp values, forcing indefinite hash retention.
The protocol tolerates bounded clock skew (±60 seconds) between client and sequencer:
| Component | Tolerance | Behavior |
|---|---|---|
| Commit expiration | ±60 seconds | Accept commits up to 60s "early" (client ahead) |
| Hash deduplication | +60 seconds | GC buffer after exp + 60000 ms |
| Bundle timeout | event timestamps | Uses event.timestamp, not wall clock |
| Session expiry | ±60 seconds | Node checks token expiry with tolerance |
Implementations SHOULD sync clocks via NTP and log warnings if skew exceeds 60 seconds.
Event
An Event is the fundamental, immutable record within an enclave.
Conceptually, an event represents a user-authorized action that has been:
- Authored and signed by a client, and
- Accepted, ordered, and finalized by a node.
An event is derived from a Commit, after validation and sequencing by a node.
Event Structure
{
id: <hex64>,
hash: <hex64>,
enclave: <enclave id>,
from: <sender_id_pub>,
type: <string>,
content: <string>,
exp: <unix timestamp>,
tags: [ <name | str1 | str2>, ... ],
timestamp: <unix time>,
sequencer: <sequencer_id_pub>,
seq: <number>,
alg: <string>,
sig: <signature over hash>,
seq_sig: <signature over hash>,
}Where:
- timestamp: time at which the event was finalized (Unix epoch milliseconds; MUST be ≥ previous event's timestamp; equal timestamps allowed, ordering determined by
seq) - sequencer: identity key of the sequencing node
- seq: monotonically increasing sequence number within the enclave (starts at 0; Manifest is seq=0)
- alg: signature algorithm used for
sig, inherited from the original commit. One of"schnorr"(default) or"ecdsa". MAY be omitted if"schnorr". - seq_sig: signature by the sequencer over the finalized event (always Schnorr)
- id: canonical identifier of the event
For all events except Migrate (forced takeover), the sequencer field MUST match the current sequencer recorded in the Manifest. If a different key signs seq_sig, the event is invalid and clients MUST reject it. Exception: Migrate in forced takeover mode — the new sequencer finalizes the Migrate event (see Migration section).
The following fields are copied directly from the original Commit:
hash— the commit hash (Event.hash == Commit.hash)enclave,from,type,content,exp,tags,alg,sig
The node adds: id, timestamp, sequencer, seq, seq_sig
Event Hash Chain
The event's cryptographic commitments are constructed as follows:
_event_hash = H(0x11, timestamp, seq, sequencer, sig)
seq_sig = schnorr(_event_hash, sequencer_priv) // always Schnorr
id = sha256(seq_sig)Note: alg does not affect the event hash chain. The sig bytes are included as-is regardless of which algorithm produced them. The sequencer always signs with Schnorr.
The resulting id:
- Commits to both client intent and node ordering
- Serves as the leaf identifier for Merkle trees and proofs
- Is immutable and globally referencable
Receipt
A Receipt is a node-signed acknowledgment proving that a commit has been accepted, sequenced, and finalized into an event.
It provides the client with the canonical event identifier and sequencing metadata, without including the event content.
Structure
{
id: <hex64>,
hash: <hex64>,
timestamp: <unix time>,
sequencer: <sequencer_id_pub>,
seq: <number>,
alg: <string>,
sig: <signature over hash>,
seq_sig: <signature over _event_hash>
}Where:
- alg: signature algorithm from the original commit (
"schnorr"or"ecdsa"). MAY be omitted if"schnorr". - sig: the client's signature from the original commit (proves client intent; algorithm per
alg) - seq_sig: the sequencer's Schnorr signature over the finalized event (proves node acceptance)
The receipt cryptographically binds client intent and node ordering, and allows the client to verify successful finalization of its commit.
Note: The Receipt intentionally omits the enclave field. The client already knows which enclave it submitted to, and omitting it provides privacy when receipts are broadcast or shared.
5. RBAC
RBAC (Role-Based Access Control) governs who can perform which operations in an enclave. The full RBAC specification is in rbac-v2.md.
Key concepts:- State (UPPER_CASE) — mutually exclusive lifecycle position. Stored as 8-bit enum in bits 0-7 of the identity bitmask. Changed via Move events. Examples: OUTSIDER, PENDING, MEMBER, BLOCKED.
- trait (lower_case) — additive capability flag with ranking. Stored as flag bits 8+ in the bitmask. Managed via Grant/Revoke/Transfer. Examples: owner, admin, muted, dataview.
- Context (PascalCase) — system-evaluated condition, not stored. Self (actor = target), Public (always matches).
- Operations — C R U D P N (positive), _C _R _U _D _P _N (deny). Deny always overrides.
Authorization: effective = (State_ops | trait_positive_ops | Self_ops | Public_ops) − deny_ops
Manifest format: 10 sections — states, traits, readers, init, moves, grants, transfers, slots, lifecycle, customs. See rbac-v2.md Section 5.
Event processing: Move, Grant, Revoke, Transfer, Gate, AC_Bundle, Lifecycle, KV (Shared/Own). See rbac-v2.md Section 8.
Application specs: Personal Enclave, DM Enclave, Group Chat.
Node role: The Node context (evaluated dynamically by comparing the actor's identity with the enclave's current sequencer) is reserved for future protocol extensions. In v1, no predefined events require Node for authorization.
P (Push) and N (Notify)
P (Push) and N (Notify) enable proactive delivery from nodes to external services or clients.
P (Push):- Node delivers the full event to identities with P permission for that event type
- Use case: DataView services that index, aggregate, or transform enclave data
- DataView does not need R permission — it receives data via push, not query
- Node delivers a lightweight notification (metadata only, not content)
- Use case: Clients that want to know when to refresh, or services tracking enclave activity
- Identities receiving P or N MUST be granted a trait with P/N ops in the manifest (e.g., admin grants a dataview trait with P permission)
- The identity's delivery endpoint MAY be registered in the Registry for well-known services
- Transport mechanism is implementation-defined
- Nodes SHOULD support HTTPS POST (webhook) delivery
- Nodes MAY support WebSocket for real-time streaming
| Type | Guarantee | Behavior |
|---|---|---|
| P (Push) | At-least-once | Node retries with exponential backoff on failure. Recipient MUST handle duplicates (dedupe by event id). |
| N (Notify) | Best-effort | Fire and forget. N is a hint; recipients can poll for missed events. |
- Initial retry: 1 second after failure
- Exponential backoff: 2x multiplier per retry
- Maximum interval: 5 minutes
- Maximum retries: 10 attempts
- After max retries: drop event, log failure, SHOULD alert enclave owner
Implementations MAY use different parameters but MUST implement retry with backoff. Nodes SHOULD NOT retry indefinitely to avoid resource exhaustion.
Push Endpoint Failure:When P delivery to a registered endpoint fails persistently (max retries exhausted):
- The endpoint registration is kept (not removed)
- The node SHOULD alert the enclave owner
- The owner can revoke the trait if the endpoint is permanently invalid
- The node retries with a fresh retry cycle on subsequent events (stateless — no failure tracking between events)
Each event's P/N delivery is independent. If delivery fails for one event, it does not affect delivery of other events. Events in the same bundle are delivered separately, each with its own retry cycle.
P (Push) Payload:The P payload is the complete Event structure (see Event Structure). No wrapper is needed — the event already contains the enclave field.
{
"enclave": "<enclave_id>",
"event_id": "<event_id>",
"type": "<event_type>",
"seq": 123,
"type_seq": 45,
"timestamp": 1706000000000
}Where:
- enclave — the enclave identifier
- event_id — the canonical event identifier
- type — the event type
- seq — the global sequence number of the event within the enclave
- type_seq — the sequence number of this event within its type (1-indexed, continuous per type)
- timestamp — the event timestamp (milliseconds)
Nodes MUST maintain a per-type sequence counter for each enclave. When an event of type T is finalized, the node increments the counter for T and assigns it as type_seq. This allows N recipients to detect missed notifications by checking continuity of type_seq.
- Starts at 1 for the first event of each type (not 0)
- Increments by 1 for each subsequent event of that type
- Never resets — even after migration, counters continue from their last value
- Each event type has an independent counter
When a recipient detects a type_seq gap (e.g., received 5, then 8):
- If R permission available: Use Query (
/query) to fetch missed events by type and sequence range. This is the preferred recovery method. - If N permission only: Gaps cannot be recovered through the protocol. N-only recipients SHOULD design their applications to tolerate gaps (e.g., treat notifications as hints rather than authoritative state). Consider upgrading to R permission if complete event history is required.
A role MAY have both P and N permissions for the same event type. P takes precedence — if P delivery succeeds, N is not sent separately. If P fails (max retries exhausted), N MAY be sent as fallback notification.
U (Update) and D (Delete)
U (Update) and D (Delete) are logical operations implemented as new events that reference prior events.
Semantics:- The event log remains append-only; original events are never mutated
- A Delete event marks a target event as logically deleted
- An Update event marks a target event as superseded and provides new content
- An event MAY be updated multiple times, forming an update chain
- Only identities with U or D permission (per schema) for that event type can issue these operations
The current status of each event (active, updated, deleted) is tracked in the Enclave State SMT alongside RBAC state (see Enclave State SMT).
When a U or D event is finalized:
- The node updates the SMT entry for the target event
- The SMT root reflects the new state
- Clients can verify event status via SMT proof
| SMT Proof Result | Interpretation |
|---|---|
| Non-membership (null) | Event is Active OR never existed |
0x00 (1 byte) | Event was Deleted |
<32-byte id> | Event was Updated to this event ID |
- Call
POST /statewithnamespace: "event_status"andkey: <event_id>→ get SMT proof - If proof value =
0x00→ event is Deleted (conclusive) - If proof value = 32-byte ID → event is Updated to that ID (conclusive)
- If proof value = null (non-membership):
- Call
POST /bundlewithevent_id→ get bundle membership proof - If proof succeeds → event is Active
- If proof fails (EVENT_NOT_FOUND) → event never existed
- Call
Clients MUST perform step 4 to distinguish Active from never-existed. The 1-byte vs 32-byte value length unambiguously distinguishes Deleted from Updated.
Content Handling:When an event is updated or deleted:
- The node SHOULD delete the original content from storage
- However, there is no guarantee of content deletion — the node operates on a best-effort basis
- Clients MUST NOT assume original content is irrecoverable
- Enclave queries: Return event + status + SMT proof (verifiable)
- DataView queries: Return event + status (trusted, no proof)
6. Enclave State
Enclave State SMT
The enclave maintains a single Sparse Merkle Tree (SMT) that stores both:
- RBAC state (namespace 0x00) — identity → bitmask (State enum + trait flags)
- Event status state (namespace 0x01) — event ID → status (active/updated/deleted)
- KV state (namespace 0x02) — key → value hash (Shared/Own slots, lifecycle, gates)
The SMT uses:
- Trimmed depth (shorter than 256 bits) for efficiency
- Flag bits to distinguish entry types (RBAC vs Event Status)
Implementation details (depth, flag encoding, proof format) are specified in smt.md.
RBAC Entries:- Path: derived from
id_pub(trimmed + flag) - Leaf value: identity bitmask (State enum in bits 0-7, trait flags in bits 8+)
- Path: derived from
event_id(trimmed + flag) - Leaf value: status + reference to U/D event
The SMT root hash represents the complete enclave state (both RBAC and event status). This single root can be used to verify any state query.
Structural Proof (Certificate Transparency)
The enclave maintains an append-only Merkle tree over the event log, providing verifiable log integrity. This follows the Certificate Transparency (CT) specification defined in RFC 9162.
Properties:- CT root: Single hash representing the entire event history AND state
- Inclusion proof: Proves a specific event exists at a given position in the log
- Consistency proof: Proves an earlier log state is a prefix of the current state
Events are grouped into bundles (see Bundle Configuration). Each bundle produces one CT leaf.
bundle = {
events: [event_id_0, event_id_1, ..., event_id_N],
state_hash: <SMT root after last event in bundle>
}Before any events (including Manifest), the SMT is empty:
empty_state_hash = sha256("")
= 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855The Manifest event is always in bundle 0. The bundle's state_hash is the SMT root AFTER Manifest's init entries have been applied.
events_root = merkle_root(events) // binary Merkle tree of event IDs
leaf_hash = H(0x00, events_root, state_hash)Where:
events_root— Merkle root of event IDs in this bundlestate_hash— SMT root AFTER the last event in this bundle is applied
For N events in a bundle:
- If N = 1:
events_root = event_ids[0](no tree needed) - If N > 1: Build a binary Merkle tree over event IDs:
- Leaf:
event_id(raw 32-byte hash, no prefix) - Internal node:
H(0x01, left, right) - If N is not a power of 2, right-pad with the last event_id until the next power of 2
- Example: 3 events → pad to 4 leaves:
[e0, e1, e2, e2]
- Leaf:
Events within a bundle are ordered by their sequence number (seq). The first event has the lowest seq, the last has the highest. This ordering is deterministic and verifiable.
Bundles are numbered sequentially starting from 0. An event's bundle membership is determined by bundle boundaries:
bundle_0contains events from seq=0 until size or timeout reachedbundle_Ncontains events fromboundary[N-1] + 1until size or timeout reachedboundary[N]= seq of last event in bundle N
event.seq belongs to bundle_N where:
boundary[N-1] < event.seq <= boundary[N]
(boundary[-1] = -1 for the first bundle)During log replay or migration, bundle boundaries are reconstructed by applying the same closing rules (size/timeout). If CT root matches after reconstruction, bundle assignment is correct.
Example:Config: bundle.size = 3, timeout = 5000ms
seq=0,1,2 (ts: 1000ms) → bundle_0, boundary[0]=2
seq=3,4,5 (ts: 3000ms) → bundle_1, boundary[1]=5
seq=6 (ts: 9000ms) → bundle_2, boundary[2]=6 (timeout hit)
Query: which bundle is seq=4?
Answer: bundle_1 (because 2 < 4 <= 5)node_hash = H(0x01, left_child, right_child)The 0x00 and 0x01 prefixes prevent second-preimage attacks by distinguishing leaf nodes from internal nodes.
The CT tree follows RFC 9162 Section 2.1 (Merkle Tree algorithm):
- Bundles are CT leaves, numbered sequentially (
bundle_0,bundle_1, ...) - Tree grows as bundles are appended
- For N bundles where N is not a power of 2: right-pad with the last bundle's
leaf_hashuntil the next power of 2 - Example: 5 bundles → pad to 8 leaves:
[b0, b1, b2, b3, b4, b4, b4, b4]
CT root (8 leaves, 5 actual bundles):
root
/ \
h01 h23
/ \ / \
h0 h1 h2 h3
/ \ / \ / \ / \
b0 b1 b2 b3 b4 b4 b4 b4
↑ ↑ ↑ ↑ ↑
actual bundles (b4 repeated as padding)This ensures deterministic CT roots across all implementations.
Proof Structure:To prove an event exists and verify state:
- Bundle membership proof — proves event_id is in the bundle's events_root
- CT inclusion proof — proves bundle is in the CT tree
With bundle.size = 1, the events_root equals the single event_id, and bundle membership proof is trivial.
The CT leaf binds each bundle to the enclave state after that bundle:
state_hash[bundle_0] = SMT root after all events in bundle 0
state_hash[bundle_N] = SMT root after all events in bundle NWithin a bundle, state changes are applied sequentially:
For events [e_0, e_1, ..., e_k] in bundle:
state = apply(state, e_0)
state = apply(state, e_1)
...
state = apply(state, e_k)
bundle.state_hash = stateState-changing events (modify SMT): Manifest, Move, Grant, Revoke, Transfer, Gate, AC_Bundle, Shared, Own, Update, Delete, Pause, Resume, Terminate, Migrate
Non-state-changing events: Content Events (app-defined customs)
Since state_hash is deterministic from the log, anyone can recompute and verify it. If a node provides incorrect state_hash values, the CT root will not match.
Note: State changes take effect immediately for authorization (in-memory). The state_hash in CT reflects the state at bundle boundaries for proof purposes.
When clients query enclave state (RBAC or event status) mid-bundle, two modes are available:
| Mode | Returns | Verifiable | Fresh |
|---|---|---|---|
verified | state_hash from last finalized bundle | ✅ Yes (CT proof) | May be stale |
current | SMT root including pending events | ❌ No proof | ✅ Fresh |
- Verified queries: Use for audits, disputes, high-stakes operations
- Current queries: Use for real-time apps (chat, collaboration)
Nodes SHOULD support both modes. The default mode is implementation-defined (SHOULD be documented).
Staleness Guidance:With default bundle.timeout = 5000 ms, verified queries may be up to 5 seconds stale. Nodes SHOULD document their bundle configuration and expected staleness.
- Client verifies an event is part of the canonical log
- Client verifies the enclave state at any point in history
- Client verifies the log they cached is still valid (consistency with current log)
- Detect if a node is presenting different log histories to different clients
- Checkpoint for migration (CT root proves both log and state)
Proof serialization formats are specified in proof.md.
Enclave Lifecycle
An enclave exists in one of four states, derived from the event log:
| State | Condition | Accepted Commits | Reads |
|---|---|---|---|
| Active | No Terminate/Migrate; last lifecycle event is not Pause | All (per RBAC) | Yes |
| Paused | No Terminate/Migrate; last lifecycle event is Pause | Resume, Terminate, Migrate only | Yes |
| Terminated | Terminate event exists | None | Until deleted* |
| Migrated | Migrate event exists | None | No obligation** |
*After Terminate, the node SHOULD delete enclave data. Reads MAY be allowed until deletion completes.
**After Migrate, the old node is not obligated to serve reads. Clients should query the new node.
State Derivation:migrated = exists(Migrate)
terminated = !migrated && exists(Terminate)
paused = !migrated && !terminated && last_of(Pause, Resume).type == "Pause"
active = !migrated && !terminated && !pausedMutual Exclusion: Terminate and Migrate are mutually exclusive terminal states. The node MUST reject:
- Migrate commit if Terminate already exists → error
ENCLAVE_TERMINATED - Terminate commit if Migrate already exists → error
ENCLAVE_MIGRATED
This prevents ambiguous state derivation. The first terminal event wins.
last_of() Semantics: last_of(Pause, Resume) returns the most recent event of either type. If neither exists, the result is null, and the paused condition evaluates to false.
Lifecycle state is stored in KV Shared (Shared("lifecycle")) in SMT namespace 0x02. The lifecycle events themselves are also recorded in the append-only log, providing an audit trail.
| Event | Transition | Reversible |
|---|---|---|
| Pause | Active → Paused | Yes |
| Resume | Paused → Active | Yes |
| Terminate | Active/Paused → Terminated | No |
| Migrate | Active/Paused → Migrated | No |
- Node MUST reject all commits except Resume, Terminate, and Migrate
- In-flight commits at the moment of Pause are rejected
- Read queries continue to work normally
- In-flight P (Push) and N (Notify) deliveries SHOULD complete (events already finalized)
Events finalized before Pause may have in-flight P/N deliveries — these SHOULD complete. No new events are finalized during Pause (except Resume/Terminate/Migrate), so no new P/N deliveries are initiated. P/N is not "paused" — there are simply no new events to trigger deliveries.
7. Predefined Events
Predefined events are event types with special semantics understood and processed by the node.
Event Categories
| Category | Description | Can be U/D |
|---|---|---|
| AC Events | Modify RBAC state (Grant, Revoke, etc.) | No |
| Content Events | Application data with no state effect | Yes |
| Update / Delete | Modify event status of Content Events | No |
- Only Content Events can be Updated or Deleted
- AC Events are irreversible — they represent state transitions
- Update/Delete events cannot themselves be Updated or Deleted
The node determines the category by event type — all AC event types and Update/Delete are predefined by the protocol.
Predefined Event Type Registry
| Type | Category | Modifies SMT | Description |
|---|---|---|---|
Manifest | Lifecycle | Yes (RBAC) | Initialize enclave |
Move | AC | Yes (RBAC) | Change identity's State |
Grant | AC | Yes (RBAC) | Add trait to identity |
Revoke | AC | Yes (RBAC) | Remove trait from identity |
Transfer | AC | Yes (RBAC) | Atomically move trait between identities |
Gate | AC | Yes (KV) | Toggle pre-RBAC event gate |
AC_Bundle | AC | Yes (RBAC) | Atomic batch of AC ops |
Shared | KV | Yes (KV) | Enclave-wide singleton key-value |
Own | KV | Yes (KV) | Per-identity key-value slot |
Update | Event Mgmt | Yes (Status) | Supersede content event |
Delete | Event Mgmt | Yes (Status) | Delete content event |
Pause | Lifecycle | Yes (KV) | Pause finalization |
Resume | Lifecycle | Yes (KV) | Resume finalization |
Terminate | Lifecycle | Yes (KV) | Close enclave |
Migrate | Lifecycle | Yes (KV) | Transfer to new sequencer |
Content Events: Any type NOT in this registry is a Content Event (e.g., message, reaction). Content Events do not modify SMT state.
For full AC event processing rules, see rbac-v2.md Section 8.
Manifest
A Manifest is a predefined event type used to initialize an enclave.
The acceptance and finalization of a Manifest event marks the creation of the enclave and establishes its initial configuration.
Type: Manifest
Manifest Commit Content
The Manifest RBAC section uses the v2 manifest format (see rbac-v2.md Section 5). Simplified example:
{
"enc_v": 2,
"states": ["MEMBER"],
"traits": ["owner(0)", "admin(1)"],
"readers": [{ "type": "MEMBER", "reads": "*" }],
"moves": [],
"grants": [
{ "event": "Grant", "operator": ["owner"], "scope": ["MEMBER"], "trait": ["admin"] },
{ "event": "Revoke", "operator": ["owner"], "scope": ["MEMBER"], "trait": ["admin"] }
],
"transfers": [],
"slots": [],
"lifecycle": [
{ "event": "Terminate", "operator": "owner", "ops": ["C"] }
],
"customs": [
{ "event": "message", "operator": "MEMBER", "ops": ["C"] },
{ "event": "message", "operator": "Self", "ops": ["U", "D"] }
],
"init": [
{ "identity": "<owner_pub>", "state": "MEMBER", "traits": ["owner", "admin"] }
],
"meta": { "description": "a simple group" },
"bundle": { "size": 256, "timeout": 5000 }
}For production manifest examples, see the app specs: Personal, DM, Group Chat.
Manifest Content Canonicalization:Unlike Content Events, the node parses Manifest content as JSON for validation. For commit hash verification:
- Client sends
contentas UTF-8 JSON string - Node hashes
contentbyte-for-byte (does not re-serialize) - Node parses JSON only for validation, not for hashing
Clients SHOULD use deterministic JSON serialization (sorted keys, no extra whitespace) for reproducibility, but this is not enforced by the protocol.
Fields
- enc_v — Version of the ENC protocol.
- states — Declared States (UPPER_CASE). See rbac-v2.md Section 5 for the full manifest format.
- traits — Declared traits with rank:
name(N). - readers — Which columns get R ops on which events.
- init — Initial identities bootstrapped at enclave creation.
- moves, grants, transfers, slots, lifecycle, customs — Authorization entries for each event category.
- meta — Optional application-defined metadata (object). The serialized
metafield (as JSON) MUST NOT exceed 4 KB (4096 bytes). Nodes MUST reject Manifests with largermetafields. - bundle — Optional bundle configuration (size, timeout). If omitted, defaults apply.
The RBAC schema is immutable after the Manifest is finalized. To change the schema, a new enclave must be created.
This ensures:
- Role bit positions remain stable for the enclave's lifetime
- RBAC proofs remain valid across the entire event history
- No ambiguity about which schema version applies to which events
The schema has no explicit version field. The enclave ID is derived from Manifest content (which includes the schema), so any schema change would produce a different enclave ID. This is intentional — changing the schema requires creating a new enclave.
Bundle Configuration
The bundle field controls how events are grouped for CT and SMT efficiency.
| Field | Type | Default | Description |
|---|---|---|---|
size | number | 256 | Max events per bundle |
timeout | number | 5000 | Max milliseconds before closing bundle |
- Close when
sizeevents accumulated, OR - Close when
timeoutms passed since bundle opened - Whichever comes first
Timeout is measured using event timestamps, not wall clock. The bundle opens when the first event's timestamp is recorded. The bundle closes when a new event arrives with timestamp >= first_event.timestamp + timeout.
Determinism: Bundle boundaries depend only on the finalized event sequence (seq order) and timestamps, which are immutable once sequenced. Out-of-order network delivery does NOT affect bundle boundaries — replaying the same log always produces identical bundles.
Note: Timeout is only checked when a new event arrives. If no events arrive, the bundle remains open indefinitely. A bundle MUST contain at least one event (empty bundles are not valid).
Idle Bundles:If an enclave receives no new events, the current bundle remains open (no timeout trigger). This is intentional: bundles are only closed when needed for new events. An open bundle has no negative effect — CT/SMT are still current in-memory. The bundle closes immediately when the next event arrives (if timeout elapsed).
Concurrent Events:The sequencer processes commits serially. "Simultaneous" arrival is resolved by the sequencer's internal ordering. Bundle boundaries are deterministic given the final event sequence.
Semantics:- All events in a bundle share the same
state_hash(SMT root after last event in bundle) - CT leaf is created per bundle, not per event
- Set
size: 1for per-event state verification (no bundling)
The state_hash is computed when the bundle closes:
- Process all events in bundle sequentially by
seqorder - Apply each state-changing event to SMT (RBAC updates, Event Status updates)
- After last event is applied, capture current SMT root
- This root becomes the bundle's
state_hash
Bundle configuration is immutable after the Manifest is finalized, like the RBAC schema.
Enclave Identifier
The enclave identifier is derived from the Manifest commit:
enclave = H(0x12, from, type, content_hash, tags)For a Manifest commit, the client MUST:
- Compute
enclaveusing the formula above - Set the
enclavefield in the commit to this value - Compute the commit
hash(which includesenclave)
1. content_hash = sha256(utf8_bytes(content))
2. enclave_id = H(0x12, from, "Manifest", content_hash, tags)
3. commit.enclave = enclave_id
4. commit_hash = H(0x10, enclave_id, from, "Manifest", content_hash, exp, tags)
5. sig = sign(commit_hash, id_priv, alg)This two-step derivation ensures the enclave ID is self-referential and deterministic.
Note: Two Manifests with identical from, content, and tags produce the same enclave ID. This is intentional — identical inputs represent the same enclave intent. To create distinct enclaves with similar configurations, include a unique value in meta (e.g., a UUID or timestamp).
The enclave ID is deterministic and independent of which node hosts it. If two nodes receive identical Manifest commits, they compute the same enclave ID. However, only one enclave with that ID should exist in the Registry at a time. Clients SHOULD verify the sequencer via Registry.
Collision Handling:If a node receives a Manifest commit for an enclave ID that already exists on that node, the node MUST reject the commit. This is detected by checking if the enclave already has a seq=0 event.
If different nodes independently create enclaves with the same ID, both enclaves are technically valid on their respective nodes. The Registry determines which is canonical for discovery purposes — clients discovering via Registry will only see one sequencer.
Registry Conflict Resolution:If two nodes attempt to register the same enclave ID in Registry:
- First valid Reg_Enclave wins (earliest
seqin Registry) - Subsequent registrations for the same
enclave_idare rejected (not superseded) - To transfer an enclave to a different node, use Migrate (not Registry re-registration)
This differs from normal Reg_Enclave superseding behavior — enclave ID conflicts are errors, not updates.
Manifest exp Field:The exp field in a Manifest commit has the same semantics as other commits: it defines the deadline by which the node must accept the commit. After enclave creation, the Manifest's exp has no ongoing effect.
Manifest Validation
Validation Order:- Verify commit structure (fields present, types correct)
- Verify commit hash and signature
- Verify enclave ID derivation matches
- Parse and validate content JSON
- Apply content-specific rules below
The node MUST reject a Manifest commit if:
enc_vis not a supported protocol versionstatesis not a non-empty array of UPPER_CASE stringstraitsis not an array ofname(rank)strings with valid non-negative integer ranksinitis empty or contains invalid entries (each must haveidentity,state,traits[])- Any identity in
initis not a valid 32-byte public key - Any State in
initis not declared instates - Any trait in
initis not declared intraits - Manifest fails any validation rule defined in rbac-v2.md Section 6
Initialization Semantics
If a node accepts the Manifest commit and returns a valid receipt, the client can conclude that:
- The enclave has been successfully initialized
- The RBAC schema and initial state are finalized
- The enclave identifier is canonical and immutable
Update
An Update event replaces the content of a previously finalized event.
Type: Update
Structure
| Field | Value |
|---|---|
| type | Update |
| content | The replacement content |
| tags | MUST include ["r", "<target_event_id>"] |
Semantics
- The target event MUST be a Content Event (AC events cannot be updated)
- The target event (referenced by
rtag) is marked as updated in the Enclave State SMT - The original content SHOULD be deleted by the node (best-effort, no guarantee)
- Only identities with U permission for the target event's type can issue an Update
- An event MAY be updated multiple times; each Update references the original event (not the previous Update)
- The SMT leaf for the original event points to the latest Update
When an Update event is finalized:
- Node looks up the target event ID from the
rtag - Node writes
SMT[target_event_id] = update_event_id - If a subsequent Update targets the same original event, the SMT entry is overwritten
The SMT always stores the most recent Update event ID for each target. Clients can follow the chain by querying the Update event, which contains the new content.
Update Lookup:All Update events target the original Content Event, not previous Updates. The SMT stores only one entry per original event, always pointing to the most recent Update. No chain following is needed — one SMT lookup returns the latest content.
Concurrent Updates in Bundle:If multiple Updates target the same original event within one bundle, they are processed serially by seq order. Each Update overwrites the previous SMT entry. Only the last Update's event_id is stored in SMT after the bundle closes.
An Update event with content: "" (empty string) is valid. This clears the content while preserving the update chain. Use case: author wants to retract content but preserve the event record.
Authorization
The node checks U permission against the target event's type, not Update. For example, if the schema grants Self the U permission for Chat_Message, only the original author can update their own messages.
Update Target Validation
The node MUST reject Update if:
- Target event does not exist
- Target event is a Delete or Update event (U/D events cannot be U/D'd)
- Target event is an AC Event
- Target event is already deleted (has Delete status in SMT)
Updating an event that was previously updated is allowed. The new Update supersedes the previous one — the SMT entry is overwritten with the new Update's event_id. The target MUST always be the original Content Event, not any intermediate Update event.
Delete
A Delete event marks a previously finalized event as logically deleted.
Type: Delete
Structure
| Field | Value |
|---|---|
| type | Delete |
| content | JSON object (see below) |
| tags | MUST include ["r", "<target_event_id>"] |
Note: The content field in events is always a UTF-8 string. The JSON structure shown is the content when parsed. The actual event stores: content: "{\"reason\":\"author\",\"note\":\"...\"}"
| Field | Required | Description |
|---|---|---|
| reason | Yes | "author" (self-deletion) or "moderator" (admin/role deletion) |
| note | No | Optional explanation (e.g., "policy violation") |
Semantics
- The target event MUST be a Content Event (AC events cannot be deleted)
- The target event (referenced by
rtag) is marked as deleted in the Enclave State SMT - The original content SHOULD be deleted by the node (best-effort, no guarantee)
- Only identities with D permission for the target event's type can issue a Delete
- Delete events provide a verifiable audit trail of who deleted content and why
Authorization
The node checks D permission against the target event's type, not Delete. For example, if the schema grants Self the D permission for Chat_Message, only the original author can delete their own messages.
If the manifest customs section defines:
{ "event": "message", "operator": "Sender", "ops": ["D"] }Alice (id_alice) can delete a message only if target_event.from == id_alice. The Sender context matches the target event's author.
If the manifest also grants admin the D permission:
{ "event": "message", "operator": "admin", "ops": ["D"] }An admin can delete any message regardless of authorship. The Sender entry only applies when the actor's identity matches the target event's from.
Delete Target Validation
The node MUST reject Delete if:
- Target event does not exist
- Target event is a Delete or Update event (U/D events cannot be U/D'd)
- Target event is an AC Event
- Target event is already deleted
Deleting an event that was previously updated is allowed. Delete targets the original Content Event, and the SMT status changes from "updated" to "deleted". All associated Update events become orphaned — they remain in the log but reference a deleted event. Clients querying the original event will see "deleted" status.
AC Events
AC (Access Control) events modify the RBAC state or lifecycle of an enclave. All AC event types are predefined by the ENC protocol and understood by the node. For full processing rules, see rbac-v2.md Section 8.
AC Event Summary
| Type | Effect |
|---|---|
| Move | Change identity's State (8-bit enum). Clears traits unless preserve: true. |
| Grant | Add trait flag to identity's bitmask. Scoped by target State. |
| Revoke | Remove trait flag. Self allowed as operator (voluntary step-down). |
| Transfer | Atomically move a trait from operator to target. |
| Gate | Toggle pre-RBAC event gate (open/closed). |
| AC_Bundle | Atomic batch of AC operations (all-or-nothing). |
Lifecycle Events
| Type | Effect |
|---|---|
| Pause | Transition to Paused state |
| Resume | Transition to Active state |
| Terminate | Transition to Terminated state |
| Migrate | Transfer to new sequencer |
Lifecycle state is stored in KV Shared (Shared("lifecycle")) in SMT namespace 0x02.
KV Events
| Type | Scope | Effect |
|---|---|---|
| Shared | Enclave-wide singleton per key | Last-write-wins mutable state |
| Own | Per-identity slot per key | Each identity writes to own slot |
KV events maintain current state in SMT namespace 0x02. History preserved in CT.
Content Events
Content Events are application-defined event types that carry data without affecting RBAC state.
- Defined in the manifest
customssection (not predefined by the protocol) - Do not modify RBAC state or enclave lifecycle
- Can be Updated or Deleted (if schema permits)
An event is a Content Event if its type is NOT in the Predefined Event Type Registry above. Determined by string comparison — no schema lookup needed.
8. Registry
The Registry is a special enclave that maps Enclave IDs to the Node endpoints that host them, and may optionally attach descriptive metadata.
Purpose
- Service discovery: resolve
enclave_id → node(s) - Identity resolution: resolve
id_pub → enclave(s) - Context lookup: understand what an enclave represents
Registry Record
A registry entry MAY include:
enclave_id— canonical enclave identifiernodes— hosting node endpoints or identifiersapp(optional) — application that created or uses the enclavecreator(optional) — identity key that initialized the enclavedesc(optional) — human-readable descriptionmeta(optional) — application-defined metadata
Trust Model
- Sequencer discovery: Registry is authoritative. Clients MUST use Registry to discover the current sequencer.
- Enclave metadata: Registry is advisory. Clients SHOULD verify enclave proofs independently.
Event Category
Reg_Node, Reg_Enclave, and Reg_Identity are Content Events within the Registry enclave. They can be Updated (to change metadata) or Deleted (to deregister) per the Registry's RBAC schema.
Update Mechanism
The Registry maintains one active entry per resource:
- Reg_Node: keyed by
seq_pub - Reg_Enclave: keyed by
enclave_id - Reg_Identity: keyed by
id_pub
A new submission supersedes any existing entry for the same key. The old entry is automatically marked as updated in SMT (no explicit Update event needed).
Registry-Specific Behavior:This implicit update is a Registry-only exception. The Registry node internally marks old entries as updated in SMT when a new entry for the same key is submitted. Normal enclaves require explicit Update events to modify event status.
SMT Update Entry:When a new Reg_Node, Reg_Enclave, or Reg_Identity supersedes an existing entry:
- SMT entry for old event:
SMT[old_event_id] = new_event_id - This follows the same pattern as explicit Update events
Clients can query the old event ID and discover it was superseded, then follow the chain to the current entry.
Implementation Note:The Registry node maintains a secondary index: key → current_event_id. On new submission:
- Look up existing event for this key
- If exists, write
SMT[old_event_id] = new_event_id - Update index to point to new event
This is internal to Registry operation — the SMT update follows normal semantics.
Querying Registry:Clients query Registry by key (seq_pub for nodes, enclave_id for enclaves, id_pub for identities). The Registry returns the latest active entry for that key. Historical entries are available in the event log but superseded entries are marked as updated in SMT.
Registry Schema
The Registry uses a minimal RBAC v2 manifest. No States (stateless enclave), two traits: owner and dataview.
{
"enc_v": 2,
"RBAC": {
"states": [],
"traits": ["owner(0)", "dataview(1)"],
"readers": [
{ "type": "Public", "reads": "*" }
],
"grants": [
{ "event": "Grant", "operator": ["owner"], "scope": ["OUTSIDER"], "trait": ["dataview"] },
{ "event": "Revoke", "operator": ["owner"], "scope": ["OUTSIDER"], "trait": ["dataview"] }
],
"transfers": [
{ "trait": "owner", "scope": ["OUTSIDER"] }
],
"slots": [],
"lifecycle": [
{ "event": "Terminate", "operator": "owner", "ops": ["C"] }
],
"customs": [
{ "event": "Reg_Node", "operator": "Public", "ops": ["C"] },
{ "event": "Reg_Node", "operator": "Sender", "ops": ["U", "D"] },
{ "event": "Reg_Node", "operator": "dataview", "ops": ["P"] },
{ "event": "Reg_Enclave", "operator": "Public", "ops": ["C"] },
{ "event": "Reg_Enclave", "operator": "Sender", "ops": ["U", "D"] },
{ "event": "Reg_Enclave", "operator": "dataview", "ops": ["P"] },
{ "event": "Reg_Identity", "operator": "Public", "ops": ["C"] },
{ "event": "Reg_Identity", "operator": "Sender", "ops": ["U", "D"] },
{ "event": "Reg_Identity", "operator": "dataview", "ops": ["P"] }
],
"init": [
{ "identity": "<registry_owner_id>", "state": "OUTSIDER", "traits": ["owner"] }
]
}- Public can read all events (
*wildcard R via readers) - dataview trait receives all events via Push (P) to serve the REST API
- Public can submit Reg_Node, Reg_Enclave, Reg_Identity (signed by submitter)
- Only the original sender can Update or Delete their own entries (Sender context)
Senderfor Reg_Node is evaluated againstcontent.seq_pubSenderfor Reg_Enclave is evaluated againstcontent.manifest_event.fromSenderfor Reg_Identity is evaluated againstcontent.id_pub- owner trait can Grant/Revoke the dataview trait (manage push endpoints)
- owner trait can Transfer ownership and Terminate
For Reg_Node: the commit's from field MUST equal content.seq_pub. One identity cannot register another node.
For Reg_Enclave: the commit's from field MUST equal content.manifest_event.from. One identity cannot register another's enclave.
For Reg_Identity: the commit's from field MUST equal content.id_pub. One identity cannot register enclaves for another.
The Registry tracks the original creator (manifest_event.from) as the default authorized identity. However, after Transfer, the new Owner can update the Registry by providing an owner_proof.
| Scenario | from field | owner_proof |
|---|---|---|
| Original creator updates | manifest_event.from | Not required |
| New Owner updates (no migration) | New Owner's key | Required (SMT proof of owner trait) |
| New Owner updates (after migration) | New Owner's key | Required (SMT proof + migrate_event) |
See the Authorization section in Reg_Enclave for verification details.
Registry Governance
Registry Owner:The registry_owner_id is the identity operating the Registry service. In the current centralized design:
- The Registry Owner is a fixed, well-known identity (e.g., protocol operator)
- The Registry Owner has standard Owner powers: can
Transfer,Pause,Resume,Terminate - The Registry Owner does NOT have special powers over individual Reg_Node/Reg_Enclave/Reg_Identity entries — those are controlled by their respective
Selfidentities
- Current design is centralized; future versions may be decentralized.
The Process of Registry
- Node Register: The node should register seq_pub and domain / IP on the registry with Reg_Node. If both domain and Ip is set, first resolve IP then the domain
- Create Enclave: The client send manifest to the node, and get receipt with sequencer, then the client knows who is hosting the enclave
- Enclave Register: Then the client can send the finalized event as content of the register commit (event type predefined as Reg_Enclave). And the registry will check the sig = event.sig in content. If it comes from the same signer, registry will accept this register
- Identity Register: The client registers its enclaves via Reg_Identity, mapping
id_pub → enclaves. The commit must be signed byid_pub. This is optional but enables discovery of an identity's enclaves
Reg_Node
Type: Reg_Node
Purpose: Register a node in the Registry for discovery.
Content Structure
{
"seq_pub": "<node's sequencer public key>",
"endpoints": [
{
"uri": "https://node.example.com:443",
"priority": 1
},
{
"uri": "https://1.2.3.4:443",
"priority": 2
}
],
"protocols": ["https", "wss"],
"enc_v": 1
}Fields
| Field | Required | Description |
|---|---|---|
| seq_pub | Yes | Node's sequencer public key |
| endpoints | Yes | Array of endpoints, ordered by priority (lower = preferred) |
| endpoints[].uri | Yes | Full URI including protocol and port |
| endpoints[].priority | No | Resolution order (default: array index) |
| protocols | No | Supported transport protocols |
| enc_v | Yes | ENC protocol version |
Validation Limits
| Field | Max |
|---|---|
| endpoints array | 10 entries |
| endpoints[].uri | 2048 characters |
| protocols array | 10 entries |
Nodes MUST reject Reg_Node commits exceeding these limits.
Authorization
- The commit MUST be signed by
seq_pub(proves ownership) fromfield MUST equalseq_pub
Update/Deregister
- To update: submit new Reg_Node (replaces previous)
- To deregister: submit Delete event referencing the Reg_Node event
Reg_Enclave
Type: Reg_Enclave
Purpose: Register an enclave in the Registry for discovery.
Content Structure
{
"manifest_event": {
"id": "<event_id>",
"hash": "<commit_hash>",
"enclave": "<enclave_id>",
"from": "<creator_id_pub>",
"type": "Manifest",
"content": "...",
"exp": 1706000000000,
"tags": [],
"timestamp": 1706000000000,
"sequencer": "<sequencer_id_pub>",
"seq": 0,
"sig": "<creator_signature>",
"seq_sig": "<sequencer_signature>"
},
"owner_proof": {
"sth": {
"t": 1706000000000,
"ts": 500,
"r": "<ct_root_hex>",
"sig": "<sth_signature_hex>"
},
"ct_proof": {
"ts": 500,
"li": 499,
"p": ["<hex64>", ...]
},
"state_hash": "<hex64>",
"events_root": "<hex64>",
"smt_proof": {
"k": "<hex42>",
"v": "<hex64>",
"b": "<hex42>",
"s": ["<hex64>", ...]
}
},
"app": "my-chat-app",
"desc": "A group chat for project X",
"meta": {}
}Fields
| Field | Required | Description |
|---|---|---|
| manifest_event | Yes | The finalized Manifest event (full event structure) |
| owner_proof | No | Proof of current Owner status (required if from ≠ manifest_event.from) |
| app | No | Application identifier |
| desc | No | Human-readable description |
| meta | No | Application-defined metadata |
Owner Proof Structure
The owner_proof field allows the current Owner to submit Reg_Enclave even if they are not the original creator. This is required after Transfer when the new Owner needs to update the Registry.
| Field | Required | Description |
|---|---|---|
| sth | Yes | Signed Tree Head from the enclave's sequencer |
| ct_proof | Yes | CT inclusion proof binding state_hash to the signed root |
| state_hash | Yes | SMT root hash at the proven tree position |
| events_root | Yes | Merkle root of event IDs in the bundle |
| smt_proof | Yes | SMT membership proof showing from has owner trait |
| migrate_event | No | Required if sequencer changed since Manifest (proves sequencer transition) |
Authorization
The Reg_Enclave commit can be authorized in two ways:
fromfield MUST equalmanifest_event.from- Registry verifies
manifest_event.sigis valid
fromfield MAY differ frommanifest_event.fromowner_prooffield MUST be present and valid- Registry verifies the submitter currently holds the owner trait
The manifest_event.sig signs the Manifest commit hash:
_content_hash = sha256(utf8_bytes(manifest_event.content))
commit_hash = H(0x10, enclave_id, from, "Manifest", _content_hash, exp, tags)
verify: sig_verify(commit_hash, manifest_event.sig, manifest_event.from, manifest_event.alg)-
Verify STH signature:
message = "enc:sth:" || be64(sth.t) || be64(sth.ts) || hex_decode(sth.r) verify: schnorr_verify(sha256(message), sth.sig, manifest_event.sequencer) -
Verify CT inclusion:
- Compute leaf hash:
H(0x00, events_root, state_hash) - Verify CT inclusion proof against
sth.rusing RFC 9162 algorithm - This binds
state_hashto the signed tree
- Compute leaf hash:
-
Verify SMT proof:
- Compute expected key:
0x00 || sha256(from)[0:160 bits](RBAC namespace, 21 bytes total) - Verify
smt_proof.kequals expected key - Verify SMT proof against
state_hash - Verify
smt_proof.vhas owner trait (bit 8) set
- Compute expected key:
-
Verify enclave binding:
- The
manifest_event.enclaveMUST match the enclave ID in Registry lookup - This prevents using an owner proof from a different enclave
- The
After migration, the sequencer changes. To update Registry after migration:
-
Include the
migrate_eventfield inowner_proof:{ "owner_proof": { "migrate_event": { "id": "<migrate_event_id>", "type": "Migrate", "content": { "new_sequencer": "<new_sequencer_pub>", ... }, "sequencer": "<new_sequencer_pub>", "seq_sig": "<new_sequencer_signature>", ... }, "sth": { ... }, "ct_proof": { ... }, ... } } -
Registry verifies the Migrate event:
migrate_event.content.new_sequencer= new sequencer public keymigrate_event.seq_sigis valid signature by new sequencermigrate_event.enclavematchesmanifest_event.enclave
-
STH signature is verified against
migrate_event.sequencer(new sequencer)
If multiple migrations occurred, only the most recent migrate_event is needed. The new sequencer's STH authenticates the current state, which includes the full history.
- The
manifest_eventprovides enclave identity and original creator - The
migrate_event(if present) proves sequencer transition - The SMT proof proves current Owner status
- All three are cryptographically bound via signatures
Derived Fields
Registry extracts:
enclave_id=manifest_event.enclavesequencer=owner_proof.migrate_event.content.new_sequencerifmigrate_eventis present, otherwisemanifest_event.sequencercreator=manifest_event.from
Update/Deregister
- To update metadata: submit new Reg_Enclave (replaces previous)
- To deregister: submit Delete event referencing the Reg_Enclave event
Reg_Identity
Type: Reg_Identity
Purpose: Register an identity's enclaves in the Registry for discovery.
Content Structure
{
"id_pub": "a1b2c3...",
"enclaves": {
"personal": "<enclave_id>",
"dm": "<enclave_id>"
}
}Fields
| Field | Required | Description |
|---|---|---|
| id_pub | Yes | Identity public key |
| enclaves | No | Map of label → enclave_id (named enclave references) |
Enclaves Map
The enclaves field is a free-form map of string keys to enclave IDs. Applications use this to associate named enclaves with an identity.
Common keys:
personal— the identity's personal enclave (see Appendix A)
The map is application-defined; the Registry stores but does not interpret the keys.
Validation Limits
| Field | Max |
|---|---|
| enclaves map | 256 entries |
| enclaves key | 64 characters |
Nodes MUST reject Reg_Identity commits exceeding these limits.
Authorization
- The commit MUST be signed by
id_pub(proves key ownership) fromfield MUST equalid_pub
Update/Deregister
- To update: submit new Reg_Identity with same
id_pub(replaces previous) - To deregister: submit Delete event referencing the Reg_Identity event
9. Migration
Migration transfers an enclave from one node to another while preserving the full event history and enclave identity.
Migrate Event
Type: Migrate
{
"new_sequencer": "<new_seq_pub>",
"prev_seq": 1234,
"ct_root": "<ct_root_at_prev_seq>"
}| Field | Required | Description |
|---|---|---|
| new_sequencer | Yes | Public key of the new sequencer node |
| prev_seq | Yes | Sequence number of the last event BEFORE the Migrate event (Migrate will have seq = prev_seq + 1) |
| ct_root | Yes | CT root at prev_seq (proves log and state before Migrate) |
- Only Owner can issue Migrate
- The commit MUST be signed by Owner
Once a node accepts a Migrate commit:
- The node MUST reject all other pending commits
- The Migrate event MUST be the final event from this sequencer
- No concurrent commits are allowed during migration
- The node MUST immediately close the current bundle after finalizing Migrate
The Migrate event does NOT need to be alone in its bundle. Events accepted before the Migrate commit may be in the same bundle. The bundle closes immediately after Migrate is finalized, regardless of bundle.size or bundle.timeout configuration.
Example with bundle.size = 10:
- Events seq=100-105 are in an open bundle
- Migrate commit arrives, finalized as seq=106
- Bundle closes immediately with events seq=100-106
- No events seq=107+ are possible from this sequencer
This ensures a clean handoff with no ambiguity about which events belong to which sequencer.
Migration Modes
Peaceful Handoff (old node online):- Owner submits
Migratecommit to old node - Old node finalizes Migrate as the last event
- Old node transfers full event log to new node
- New node verifies log matches
ct_root - New node continues sequencing; next event will be seq =
prev_seq + 2(Migrate event wasprev_seq + 1) - Owner updates Registry (
Reg_Enclave)
- Owner or Backup has a copy of the event log
- Owner signs
Migratecommit (unfinalized) - Owner submits log + unfinalized commit to new node
- New node verifies log integrity (see verification below)
- New node finalizes the Migrate event (special case)
- New node becomes the sequencer
- Owner updates Registry (
Reg_Enclave)
In forced mode, the sequencer field of the Migrate event will be the NEW node, not the old one. This is the only event type where sequencer discontinuity is allowed.
Before finalizing Migrate, the new node MUST rebuild state from scratch:
- Replay all events from the log, computing SMT state after each state-changing event
- Verify computed SMT root matches
state_hashin final bundle - Verify the
fromfield of the Migrate commit has owner trait set in the computed SMT (RBAC namespace) - Recompute CT root — MUST match
ct_rootin Migrate commit - Verify commit signature (
sig) is valid for the computed commit hash - Verify
prev_seqequals the last event's sequence number in the log
If any check fails, the new node MUST reject the migration. This full replay ensures the new node has a correct, verified copy of enclave state.
Checkpoint Verification
The ct_root field serves as a checkpoint:
- CT root commits to both the event log AND state (via
state_hashin leaves) - New node recomputes CT root from received log
- If mismatch, migration is rejected
Split-Brain Prevention
After migration:
- Old node's sequencer key is no longer valid
- Any events finalized by old node after Migrate are invalid
- Registry is authoritative for sequencer discovery
- Clients SHOULD check Registry periodically
If a client queries the old node after Migrate:
- Old node MAY still serve read requests (CT proofs, state proofs) — data is valid
- Old node MUST reject new commits (new sequencer handles writes)
- Client discovers migration via Registry lookup or
ENCLAVE_NOT_FOUNDon commit - Client migrates to new node for subsequent operations
No explicit "migrated" error is required on reads; normal operation continues until the client syncs with Registry.
Backup Pattern
To enable forced takeover, ensure someone has the full event log:
Option 1: Owner maintains backup- Owner's client stores all events locally
- Define a custom role with P (Push) permission for all event types
- Assign to a backup service
Schema example — define a backup trait with P ops on all events:
{ "event": "*", "operator": "backup", "ops": ["P"] }The wildcard * means the backup trait receives push for ALL event types. This enables disaster recovery if the node goes offline.
Important: Forced takeover requires Owner signature on the Migrate commit. If the Owner is offline and cannot sign:
- Forced takeover is blocked (intentional security — only Owner can authorize sequencer changes)
- Mitigation: ensure Owner's client/key is highly available, or use Transfer(owner) before sequencer goes offline
10. Node API
TODO: Endpoints and request formats assigned to Tomo
Error Response Format
When a node rejects a commit or request, it MUST return an error response:
{
"type": "Error",
"code": "<ERROR_CODE>",
"message": "<human-readable description>",
...additional fields specific to the error...
}| Field | Required | Description |
|---|---|---|
| type | Yes | Always "Error" |
| code | Yes | Machine-readable error code (UPPER_SNAKE_CASE) |
| message | Yes | Human-readable description |
| additional | No | Error-specific context (see below) |
| Code | Context Fields | Description |
|---|---|---|
UNAUTHORIZED | — | Sender lacks required permission |
STATE_MISMATCH | expected, actual | Move target's current State does not match from |
RANK_INSUFFICIENT | — | Operator's rank is not strictly less than target's rank |
INVALID_STATE_FOR_GRANT | — | Target's State is not in Grant/Revoke scope |
INVALID_STATE_FOR_TRANSFER | — | Target's State is not in Transfer scope |
INVALID_TRANSFER_TARGET | — | Transfer target is the operator (self-transfer) |
TRAIT_ALREADY_HELD | — | Transfer target already holds the trait |
INVALID_LIFECYCLE_STATE | — | Lifecycle transition not valid from current state |
AC_BUNDLE_FAILED | failed_index, reason | AC_Bundle operation failed at index |
COMMIT_EXPIRED | — | Commit exp time has passed |
DUPLICATE_COMMIT | — | Commit hash already processed |
EVENT_DELETED | — | Target event has been deleted (Self/D on deleted event) |
ENCLAVE_PAUSED | — | Enclave is paused, only Resume/Terminate/Migrate accepted |
ENCLAVE_TERMINATED | — | Enclave is terminated, no events accepted |
ENCLAVE_MIGRATED | — | Enclave has migrated to another node |
ENCLAVE_NOT_FOUND | — | Enclave ID not found on this node |
11. Templates
Predefined RBAC templates for common enclave patterns.
When use_temp is set in Manifest content, the node uses the referenced template instead of the explicit schema field.
| Template | Description |
|---|---|
none | No template; use explicit schema |
In ENC v1, only none is supported. Additional templates (e.g., chat, forum, personal) are planned for future versions. Nodes MUST reject Manifests with unrecognized use_temp values. For the recommended personal enclave schema using explicit none template, see Appendix A.
Appendix A: Design Patterns
This section is non-normative. It describes common usage patterns, not protocol requirements. Detailed RBAC designs for each pattern are in Personal Enclave, DM Enclave, and Group Chat.
Shared Enclave
A Shared Enclave is an enclave whose data and access-control are intended to be jointly used by multiple identities.
Typical characteristics- Multi-writer / multi-reader by design
- RBAC is centered on group roles (e.g., Admin/Member/Moderator)
- Data is considered collective (not owned by a single identity)
- Group chat enclave
- A protocol-level registry/directory enclave
- DAO / project coordination enclave
- Shared public feed with moderators
Personal Enclave
For the full RBAC v2 design, see Personal Enclave.
A Personal Enclave is an enclave whose data is logically owned and controlled by a single identity, even if many others can read or contribute under permission.
Typical characteristics- A single "owner" identity is the final authority for RBAC and lifecycle
- Designed for portable identity-scoped data with a unified API
- Others may write into it (e.g., comments, reactions) but the enclave remains owner-governed
- A user's posts / profile / settings enclave
- A user's inbox / notifications enclave
- Personal "data vault" enclave used across apps
For the full RBAC v2 manifest, state transitions, and event-operator matrix, see Personal Enclave.
Registration
After creating a personal enclave, the owner SHOULD register their identity via Reg_Identity (see Section 8) with the enclaves.personal field pointing to the personal enclave. This enables other users to discover the enclave by public key:
id_pub → Reg_Identity → enclaves.personal → enclave_id → Reg_Enclave → nodeGroup Chat
A Group Chat Enclave is a shared space for multi-party messaging. It uses the RBAC v2 State/trait model with PENDING, MEMBER, and BLOCKED states, plus owner/admin/muted/dataview traits. Multiple join paths (application, auto-join, direct invite), gated transitions, and trait-based moderation. Lazy MLS encryption with tree ratchet for O(log N) epoch distribution.
For the full design pattern, see Group Chat Enclave.
How to Choose
Use Shared when:
- The enclave represents a group object (a room, project, DAO, registry)
- Governance and policy must be collective or moderator-driven
- Many identities are primary contributors
Use Personal when:
- The enclave represents identity-scoped state (my profile, my posts, my inbox)
- You want data to be portable across apps
- One identity should remain the owner-of-record
DM (Direct Message)
For the full RBAC v2 design with per-contact epoch encryption, see DM Enclave.
DM is a messaging pattern built on personal enclaves, not a shared conversation enclave.
Each identity maintains a personal mailbox enclave that receives incoming messages.
How it works- Alice sends a message event to Bob's personal enclave
- Bob replies by sending a message event to Alice's personal enclave
- Bob's client MAY also store a local copy of his outgoing reply in Bob's own enclave
- Messages are asymmetric by default (each direction is a separate write)
- Each participant maintains a complete local history inside their own enclave
- Ownership and RBAC are enforced per enclave, independently
- No shared or mutually-owned log is required
DM is modeled as:
"write into the recipient's enclave, optionally mirror into the sender's enclave"
—not:
"append to a shared conversation log"
Important Note
There is no mandatory "type" field. You MAY include a hint in metadata (e.g., meta.enclave_kind = "shared" | "personal"), but clients must not rely on it for security decisions.
Appendix B: Event Type Registry
Machine-readable registry of predefined event types. Content Events are any type NOT in this list.
{
"version": 2,
"predefined_types": {
"ac_events": [
"Manifest",
"Move",
"Grant",
"Revoke",
"Transfer",
"Gate",
"AC_Bundle"
],
"kv_events": [
"Shared",
"Own"
],
"lifecycle_events": [
"Pause",
"Resume",
"Terminate",
"Migrate"
],
"mutation_events": [
"Update",
"Delete"
],
"registry_events": [
"Reg_Node",
"Reg_Enclave",
"Reg_Identity"
]
}
}To determine if an event is a Content Event:
is_content_event = type NOT IN any predefined_types categoryNote: registry_events (Reg_Node, Reg_Enclave, Reg_Identity) ARE Content Events within the Registry enclave. They can be Updated or Deleted per the Registry's RBAC schema. They are listed separately for documentation purposes.
For event processing rules, see rbac-v2.md Section 8.