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

ENC Protocol Specification

ENC (Encode, Encrypt, Enclave) is a protocol for building log-based, verifiable, sovereign data structures with Role-Based Access Control. This document specifies the core protocol: cryptographic primitives, the Commit / Event / Receipt wire protocol, event semantics (P / N / U / D), and the enclave lifecycle (Manifest / Update / Delete / Pause / Resume / Terminate). Companion docs cover RBAC v2, the storage layer (smt.md, ct.md, zk.md), enclave migration, the node API, and the enclave profiles / confidentiality plugins.


Table of Contents

  1. Protocol Summary
  2. Trust Model
  3. Enclave
  4. Node
  5. Client
  6. Signature Schemes
  7. Hash Function
  8. Hash Encoding
  9. Hash Prefix Registry
  10. Identity Key
  11. Delegated Sub-keys
  12. Wire Format
  13. Commit
  14. Event Finalization
  15. Event
  16. Receipt
  17. P (Push) and N (Notify)
  18. U (Update) and D (Delete)
  19. Event Categories
  20. Predefined Event Type Registry
  21. AC Events
  22. Content Events
  23. Event Type Registry (Appendix)
  24. Enclave Lifecycle
  25. Manifest
  26. Update
  27. Delete
  28. Templates
  29. Migration

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.
Data Access Methods:

A DataView can receive enclave data through three methods, each requiring a State or trait assignment:

MethodPermissionDescription
QueryR (Read)DataView queries the enclave directly as a member
PushPNode delivers full events to DataView proactively
NotifyNNode 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, and audit trails.


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 ct.md)

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 sequencer field.
  • Sequencer discovery is out of scope for the core protocol. The Registry enclave is one discovery mechanism (see enclaves/registry.md); other mechanisms (DNS, well-known URLs, bootstrap config) are equally valid.
Responsibilities:
  • 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.
Sequencer change:

Note: Multi-sequencer (consensus-based) models are out of scope for v2.


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 can be an app, service, or device component.
  • id_priv can be held directly or accessed via a secure signer (e.g., OS keystore, HSM, enclave, wallet).

Identity Custody

An identity has a custody descriptor naming how id_priv is reached by the client. Two values are defined:

local       — the client holds `id_priv` directly in memory (or in an
              equivalent process-local store accessible synchronously
              and without consent gating).
delegated   — the client has no direct access to `id_priv`. Sign /
              encrypt / decrypt are exposed as an oracle by an
              external signer (NIP-07 / NIP-46, WebAuthn, OS keystore
              with biometric consent gating, KMS / HSM, threshold
              signer, hardware wallet, …). Every operation that needs
              `id_priv` is one round trip through the oracle.

Custody is orthogonal to the signature scheme and to the ciphersuite. A local-custody client and a delegated-custody client running the same signature scheme produce byte-identical signatures; the same plugin running the same suite produces byte-identical ciphertexts under either custody. The visible difference is operational: under delegated custody every id_priv-using operation requires an online round-trip to the signer, and the signer MAY refuse (consent gating).

Signer oracle interface

When custody is delegated, the signer exposes exactly these four operations:

OperationInputOutputNote
getPublicKey()32-byte id_pubReturned per-identity; the signer MAY manage many identities.
sign(message)message bytessignature (per alg)One online round-trip; the signer MAY require user consent.
encrypt(peer, plaintext, suite)peer pubkey, plaintext bytes, suite idciphertext per the suite's wire framingEncrypts under the suite's KDF chain rooted in ECDH(id_priv, peer). One round-trip per ciphertext.
decrypt(peer, ciphertext, suite)peer pubkey, ciphertext, suite idplaintext bytesOne round-trip per ciphertext; the signer MAY refuse or rate-limit.

A signer that does not expose all four operations is treated as local-only for the missing operations — typically a hardware signer that exposes sign but not encrypt/decrypt cannot serve a delegated-custody encrypted enclave at all.

Custody-conditional invariants

The plugin catalog's "CT-replayable from identity_priv" guarantee (see plugins.md §Capability matrix) is custody-conditional:

  • Under local custody: every plugin in the catalog is CT-replayable from identity_priv offline. The "NO" column on the Post-compromise security vs. identity_priv row reflects the protocol's accepted trust-root tradeoff.
  • Under delegated custody: identity_priv is unreachable, so offline bulk re-derivation is impossible. Recovery is decrypt-on-demand: one signer round-trip per ciphertext, consent-gated by the oracle. Plugins in the catalog MUST NOT assume offline bulk re-derivation.

The Multi-device via identity_priv row (capability axis 7) and the Post-compromise security vs. identity_priv row (capability axis 10) are likewise custody-conditional: their guarantees, as stated in the matrix, apply only under local custody. A delegated client that wants to share decryption across devices does so by sharing access to the signer oracle, NOT by exporting id_priv.

Authentication of custody

Custody is NOT advertised on the wire, NOT carried in commit signatures, and NOT inferable from the ciphertext. It is a client-local property — the receiver cannot tell from a ciphertext whether the sender's id_priv is local or delegated. The wire shape is the same under both custodies, so a bridge or a custody migration is invisible to peers.

The signer interface MAY change over an identity's lifetime (a fresh wallet may start local and later move to a hardware signer; a Nostr-bridged identity is delegated from the start). Plugins MUST NOT key any of their on-the-wire framing on custody.


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

algSchemeSpecificationSignature FormatNote
"schnorr"SchnorrBIP-34064 bytes (R
"ecdsa"ECDSASEC 1 v2 §4.1 + RFC 697964 bytes (r

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).
Deterministic Signing:

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 r and s MUST be in the range [1, n-1] where n is 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 from field 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_pub derivation.

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:

  1. 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.
  2. 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.
  3. alg field integrity — If alg is 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. The notation:

sha256([0x10, enclave, from, type, content_hash, exp, tags])

it means:

sha256(cbor_encode([0x10, enclave, from, type, content_hash, exp, tags]))
CBOR Encoding Rules:
  • 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.
  • The tags field is a CBOR array of tags, and each tag is itself a definite-length CBOR array of one or more CBOR text strings. A tag's element count is its actual arity — it is NOT fixed at 2. Tags are variable-length (Nostr-style): a tag MAY carry more than a [key, value] pair (e.g. [mention, pubkey, relay]), and every element participates in the hash pre-image. Encoders MUST NOT assume a fixed tag arity nor truncate elements past index 1; the per-tag array header MUST carry the tag's true length.

Hash Prefix Registry

To prevent hash collisions between different data types, all hash operations use a unique single-byte prefix:

PrefixPurpose
0x00CT leaf (RFC 9162)
0x01CT internal node (RFC 9162)
0x10Commit hash
0x11Event hash
0x12Enclave ID
0x20SMT leaf
0x21SMT internal node

Prefixes 0x020x0f, 0x130x1f, and 0x220x2f are reserved for future use.

String Prefixes for Signatures:

Signature contexts use string prefixes (not byte prefixes) for domain separation:

ContextPrefix String
STH signature"enc:sth:"
Session token"enc:session:"

String prefixes are concatenated with data before hashing: sha256(prefix || data).


Identity Key

An Identity Key is a 256-bit public key (id_pub) used to represent an identity in the ENC protocol.


Delegated Sub-keys

An identity MAY delegate a scoped, revocable capability to a separate sub-key — for example, to authorize an autonomous agent or a per-device operating key without exposing the identity key. The sub-key holds its own keypair; the identity issues a grant binding it.

Cert (Cosigned Sub-key Authorization)

A sub-key authorization is a cosigned cert — both the parent identity and the sub-key sign, so a verifier knows the identity authorized the sub-key AND the sub-key holder accepted the binding. The cosignature shape is required so MetaMask-style ECDSA-only parent identities (which sign via EIP-191 and cannot perform BIP-340 Schnorr) can authorize Schnorr-native sub-keys without exposing the parent private key.

Wire shape:

{
  "subkey":   "<32-byte hex; sub-key's x-only public key>",
  "parentId": "<32-byte hex; parent identity x-only public key>",
  "expiry":   1706000000,
  "ackSig":   "<sub-key Schnorr signature; see below>",
  "authSig":  "<parent identity recoverable ECDSA signature in EIP-191; see below>"
}
FieldDescription
subkeyThe sub-key's 32-byte x-only public key (the operating key the sub-key holder controls).
parentIdThe parent identity's id_pub (32-byte x-only). On verification this MUST equal commit.from — a cert binds the sub-key to one specific parent identity.
expiryUnix-seconds expiry. The cert is rejected if expiry < now.
ackSigSub-key acknowledgment: schnorr_sign("ENC sub-key ack v1|<parentId hex>|<expiry>", subkey_priv). Proves the sub-key holder accepted the binding to this parent. Domain separator string: "enc:subkey-ack:v1|".
authSigParent authorization: ECDSA-recoverable_sign( eip191_hash("enc:authorize-subkey:v1|<subkey hex>|<expiry>"), parent_priv ). 65 bytes (`r

The cert as a whole is a one-shot artifact: the parent doesn't need to be online for the sub-key to operate; the sub-key submits the cert alongside the commit and the node verifies it offline.

Cert-Authorized Commits

When a commit is signed by a sub-key (not the parent identity directly), the commit MUST carry a cert field:

{
  "hash":     "<hex64>",
  "enclave":  "<hex64>",
  "from":     "<parentId hex>",
  "type":     "<string>",
  "content":  "<string>",
  "content_hash": "<hex64>",
  "exp":      1706000000000,
  "tags":     [...],
  "cert":     { subkey, parentId, expiry, ackSig, authSig },
  "sig":      "<Schnorr signature over commit.hash by the SUB-KEY>",
  "alg":      "schnorr"
}
  • commit.from is the parent identity (the cert's parentId). This is what RBAC, reg_identity lookups, and CT membership are keyed to — the sub-key's existence is transparent to authorization at that layer.
  • commit.sig is the SUB-KEY's Schnorr signature over commit.hash (using the standard commit hash construction). The sub-key signs the same digest the parent identity would have, with the cert proving the parent delegated this capability.
  • commit.alg for the sub-key signature is "schnorr" (sub-keys are always Schnorr — see Status note below).

Verification

A verifier accepts a cert-signed commit only if all hold:

  1. commit.cert.parentId equals commit.from (case-insensitive hex).
  2. commit.cert.expiry > now() (current wall-clock seconds).
  3. commit.cert.ackSig is a valid Schnorr signature by commit.cert.subkey over sha256("enc:subkey-ack:v1|" || parentId_hex || "|" || expiry_decimal).
  4. commit.cert.authSig is a valid recoverable ECDSA signature over eip191_hash("enc:authorize-subkey:v1|" || subkey_hex || "|" || expiry_decimal), AND the recovered uncompressed public key's x-only encoding equals commit.cert.parentId.
  5. commit.sig is a valid Schnorr signature by commit.cert.subkey over commit.hash.

If any check fails, the commit is rejected. The alg field on the outer commit is "schnorr" (the sub-key's signature scheme); alg = "ecdsa" is NOT compatible with cert-authorized commits — direct-ECDSA commits use the parent ECDSA path (no cert), not cosign.

Rotation, Revocation

  • Rotation: issue a new cert for a new sub-key; the parent identity (id_pub) is unchanged so peers continue to recognise the same identity. Sub-keys rotate freely beneath the stable identity.
  • Revocation: a cert is revoked by expiry, or explicitly via a future revocation event (post-v2 — see Status note below). A compromised sub-key thus has a bounded blast radius and never exposes the parent private key.

Note — derivation vs. authorization. A cert proves the identity authorized the sub-key, and (via ackSig) that the sub-key is held. It does not prove the sub-key was derived from the identity key; an x-only-only authorization cannot enforce derivation. Verifier-enforced derivation requires a key-tweak relationship or a zero-knowledge proof and is out of scope here.


Wire Format

Transport Encoding:

The normative wire format is JSON. CBOR is used only for hash computation, not transport.

JSON Encoding:

When serializing for transport or storage as JSON:

TypeEncodingExample
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)
RBAC bitmaskHex string with 0x prefix"0x100000002"
ContentUTF-8 string"hello world"
Binary in contentBase64 encoded"SGVsbG8="
IntegersJSON number1706000000

Consistency: All bitmask values (State + trait flags) in JSON (event content, proofs, API responses) MUST use the 0x hex prefix format.

CBOR Encoding (commit / event pre-image):

When serializing for hashing or binary transport:

TypeEncoding
Hash, public key, signatureCBOR byte string (major type 2)
Content, typeCBOR text string (major type 3)

RBAC bitmask (SMT leaf, NOT CBOR): RBAC bitmasks (State + trait flags) are NOT encoded as CBOR; they are written into SMT leaves as 32-byte big-endian unsigned-integer byte strings and hashed via leaf_hash = H(0x20 || key || value) (raw concatenation, not CBOR-wrapped). See smt.md §Role-Based Access Control. Outside the SMT (event content, JSON proofs, API responses), bitmasks render as 0x-prefixed hex per the JSON table above.


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: <"schnorr" | "ecdsa">,
  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: OPTIONAL signature scheme — "schnorr" (default if absent) or "ecdsa". See Signature Schemes. NOT bound in hash (see §"alg field integrity"); tampering causes signature verification to fail, not forgery.
  • sig: signature by from, proving authorship

Commit Hash Construction

This formula applies to ALL event types (Manifest, Grant, message, etc.):

_content_hash = sha256(utf8_bytes(content))
hash = H(0x10, enclave, from, type, _content_hash, exp, tags)
sig = schnorr(hash, from_priv)

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.

Binary Data in Content:

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

Wire Format: The client transmits BOTH content AND content_hash in the commit (see §Commit Structure — content_hash is a required commit field, bound in the signature via the CBOR pre-image at byte position 5). On receipt the node MUST recompute expected = sha256(utf8_bytes(content)) and reject the commit with CONTENT_HASH_MISMATCH if expected ≠ content_hash. Without this server-side check, a misbehaving client could attach a signed content_hash to a different content payload and the receipt would still validate; the node's verification closes that gap.

Note on the finalized event: The event MUST carry content_hash as a clear-text field (it's inherited from the commit per §Event Structure). Recipients verify the commit signature against the same pre-image; they MAY also recompute sha256(utf8_bytes(content)) and confirm the match for defense-in-depth.

Content Integrity:

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.

Application-Level Canonicalization:

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.

Tags MUST be replicated unchanged. Nodes store the tags array byte-for-byte alongside the rest of the event and return it on every read path (HTTP GET, WebSocket Event frames, push delivery, backfill). Tags carry plugin-level routing data (["to", "<id>"] on dm:sent, ["enclave_id", "<id>"] on cross-enclave notice, etc.); plugin decryption fails if tags are dropped or mutated in the store. An implementation that backs events with a relational table MUST include a tags column (or equivalent encoding) so storage round-trip preserves them — the commit hash binds them into event identity, so any divergence corrupts every downstream proof.

Example:
[
  ["auto-delete", "1706000000000"],
  ["r", "abc123...", "reply"]
]
Predefined Tags:
TagDescription
rReference — references another event. Format: ["r", "<event_id>", "<context>"]. See context values below.
auto-deleteAuto-delete timestamp — node SHOULD delete the event content after this Unix timestamp (milliseconds, same as exp and timestamp)
r Tag Context Values:
ContextMeaning
(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.

Auto-delete vs Delete event:
AspectAuto-delete (tag)Delete (event)
MechanismNode silently removes content after timestampExplicit Delete event updates SMT
VerifiableNo — trust-basedYes — auditable proof
SMT stateUnchanged (remains "active")Updated to "deleted"
Audit trailNoYes (who, when, why)
Use caseEphemeral content, disappearing messagesCompliance, 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 and SMT:

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 while event metadata can remain. Auto-delete is trust-based, not verifiable via SMT proofs.

Future protocol or application schemas can define additional predefined tags.


Event Finalization

Upon receiving a valid commit, a node performs:

  1. Expiration check — reject if exp < current time
  2. Deduplication check — reject if commit hash was already processed (see Replay Protection)
  3. RBAC authorization check — verify sender has C permission for this event type
  4. 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.

Implementation:
  • Node maintains a set of accepted commit hashes per enclave
  • Before accepting a commit, check if hash exists in the set
  • If duplicate, reject the commit
  • Only add hash to set AFTER successful acceptance
  • Hashes MAY be garbage collected after exp + 60000 ms (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 State or trait).

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 → reject

The 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.

Clock Skew Tolerance:

The protocol tolerates bounded clock skew (±60 seconds) between client and sequencer:

ComponentToleranceBehavior
Commit expiration±60 secondsAccept commits up to 60s "early" (client ahead)
Hash deduplication+60 secondsGC buffer after exp + 60000 ms
Bundle timeoutevent timestampsUses event.timestamp, not wall clock
Session expiry±60 secondsNode 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:

  1. Authored and signed by a client, and
  2. 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: <"schnorr" | "ecdsa">,
  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).
  • seq_sig: signature by the sequencer over the finalized event
  • id: canonical identifier of the event
Sequencer Continuity:

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

Field inheritance from Commit:

The following fields are copied directly from the original Commit:

  • hash — the commit hash (Event.hash == Commit.hash).
  • enclave, from, type, content, exp, tags, 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)
id = sha256(seq_sig)

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>,
  sig: <signature over hash>,
  seq_sig: <signature over _event_hash>
}

Where:

  • sig: the client's signature from the original commit (proves client intent)
  • seq_sig: the sequencer's 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.

alg on receipts: The Receipt also intentionally omits alg. The receipt's sig IS the commit's sig (proving client intent) and is verified by the same alg the client used; the client receiving the receipt already knows that value. A third-party verifier obtains alg from the finalized event (which DOES inherit alg from the commit per §Event Structure), then verifies sig against the event's hash per §Signature Schemes. The receipt's seq_sig is always Schnorr (sequencer signature — see §Sequencer and Server Signatures) and needs no alg discriminator.


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
N (Notify):
  • Node delivers a lightweight notification (metadata only, not content).
  • Use case: Clients that want to know when to refresh, or services tracking enclave activity
Registration:
  • 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 delivery endpoint is carried in the Grant event itself (endpoint field on the Grant content); the node registers it as operational state at Grant time. The Registry enclave is NOT consulted for P/N destinations.
Transport:

::: extension-point id=push-notify-transport class=deployment_choice reason: P/N delivery uses operator-deployment transport (HTTPS webhook, WebSocket, queue); the wire shape of the delivered event is unchanged across transports

The transport mechanism for Push (P) / Notify (N) delivery is a deployment choice. Nodes SHOULD support HTTPS POST (webhook) delivery; nodes MAY additionally support WebSocket for real-time streaming. :::

Delivery Guarantees:
TypeGuaranteeBehavior
P (Push)At-least-onceNode retries with exponential backoff on failure. Recipient MUST handle duplicates (dedupe by event id).
N (Notify)Best-effortFire and forget. N is a hint; recipients can poll for missed events.
P (Push) Retry Policy:
  • 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)
P/N Delivery Independence:

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.

N (Notify) Payload:
{
  "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)
Per-Type Sequence Counter:

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.

type_seq Rules:
  • 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.
Gap Recovery:

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.
DataView with P+N:

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.
Event Status State:

The current status of each event (active, updated, deleted) is tracked in the Enclave State SMT alongside RBAC state (see smt.md §Enclave State SMT).

When a U or D event is finalized:

  1. The node updates the SMT entry for the target event.
  2. The SMT root reflects the new state
  3. Clients can verify event status via SMT proof
Content Integrity Proofs:
SMT Proof ResultInterpretation
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
Verification Flow:
  1. Call POST /state with namespace: "event_status" and key: <event_id> → get SMT proof
  2. If proof value = 0x00 → event is Deleted (conclusive)
  3. If proof value = 32-byte ID → event is Updated to that ID (conclusive)
  4. If proof value = null (non-membership):
    • Call POST /bundle with event_id → get bundle membership proof
    • If proof succeeds → event is Active
    • If proof fails (EVENT_NOT_FOUND) → event never existed

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.
Querying:
  • Enclave queries: Return event + status + SMT proof (verifiable)
  • DataView queries: Return event + status (trusted, no proof)

Event Categories

CategoryDescriptionCan be U/D
AC EventsModify RBAC state (Grant, Revoke, etc.)No
Content EventsApplication data with no state effectYes
Update / DeleteModify event status of Content EventsNo
Key rules:
  • 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

TypeCategoryModifies SMTDescription
ManifestLifecycleYes (RBAC)Initialize enclave
MoveACYes (RBAC)Change identity's State
GrantACYes (RBAC)Add trait to identity
RevokeACYes (RBAC)Remove trait from identity
TransferACYes (RBAC)Atomically move trait between identities
GateACYes (KV)Toggle pre-RBAC event gate
AC_BundleACYes (RBAC)Atomic batch of AC ops
SharedKVYes (KV)Enclave-wide singleton key-value
OwnKVYes (KV)Per-identity key-value slot
UpdateEvent MgmtYes (Status)Supersede content event
DeleteEvent MgmtYes (Status)Delete content event
PauseLifecycleYes (KV)Pause finalization
ResumeLifecycleYes (KV)Resume finalization
TerminateLifecycleYes (KV)Close enclave
MigrateLifecycleYes (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/events.md.


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/events.md.

AC Event Summary

TypeEffect
MoveChange identity's State (8-bit enum). Clears traits unless preserve: true.
GrantAdd trait flag to identity's bitmask. Scoped by target State.
RevokeRemove trait flag. Self allowed as operator (voluntary step-down).
TransferAtomically move a trait from operator to target.
GateToggle pre-RBAC event gate (open/closed).
AC_BundleAtomic batch of AC operations (all-or-nothing).

Lifecycle Events

TypeEffect
PauseTransition to Paused state
ResumeTransition to Active state
TerminateTransition to Terminated state
MigrateTransfer to new sequencer

Lifecycle state is stored in KV Shared (Shared("lifecycle")) in SMT namespace 0x02.

KV Events

TypeScopeEffect
SharedEnclave-wide singleton per keyLast-write-wins mutable state
OwnPer-identity slot per keyEach 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 customs section (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.


Event Type Registry (Appendix)

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"
    ]
  }
}
Usage:

To determine if an event is a Content Event:

is_content_event = type NOT IN any predefined_types category

Note: 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.md Section 8.


Enclave Lifecycle

An enclave exists in one of four states, derived from the event log:

StateConditionAccepted CommitsReads
ActiveNo Terminate/Migrate; last lifecycle event is not PauseAll (per RBAC)Yes
PausedNo Terminate/Migrate; last lifecycle event is PauseResume, Terminate, Migrate onlyYes
TerminatedTerminate event existsNoneUntil deleted*
MigratedMigrate event existsNoneNo 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 && !paused

Mutual 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.

Lifecycle Events:
EventTransitionReversible
PauseActive → PausedYes
ResumePaused → ActiveYes
TerminateActive/Paused → TerminatedNo
MigrateActive/Paused → MigratedNo
Behavior in Paused State:
  • 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).
P/N Delivery in Paused State:

Events finalized before Pause can already have in-flight P/N deliveries; the paused-state rule above preserves completion for those deliveries. 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.


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/manifest.md). 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 content as UTF-8 JSON string
  • Node hashes content byte-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/manifest.md for the full manifest format.
  • traits — Declared traits with rank: name(N).
  • readers — Which columns grant read authority 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 meta field (as JSON) MUST NOT exceed 4 KB (4096 bytes). Nodes MUST reject Manifests with larger meta fields.
  • bundle — Optional bundle configuration (size, timeout). If omitted, defaults apply.
Schema Immutability:

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
No Schema Version Field:

The schema has no explicit version field. The enclave ID is derived from Manifest content (which includes the schema), so any schema change produces a different enclave ID; changing the schema therefore requires creating a new enclave.

Bundle Configuration

The bundle field controls how events are grouped for CT and SMT efficiency.

FieldTypeDefaultDescription
sizenumber256Max events per bundle
timeoutnumber5000Max milliseconds before closing bundle
Bundle closing rules:
  • Close when size events accumulated, OR
  • Close when timeout ms passed since bundle opened
  • Whichever comes first
Timeout Clock:

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); 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: 1 for per-event state verification (no bundling).
state_hash Computation Timing:

The state_hash is computed when the bundle closes:

  1. Process all events in bundle sequentially by seq order
  2. Apply each state-changing event to SMT (RBAC updates, Event Status updates)
  3. After last event is applied, capture current SMT root
  4. This root becomes the bundle's state_hash
Immutability:

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:

  1. Compute enclave using the formula above
  2. Set the enclave field in the commit to this value
  3. Compute the commit hash (which includes enclave)
Derivation Order (critical for implementation):
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)
   where sign() dispatches per `alg`:
     - "schnorr" (or absent) : schnorr_sign(commit_hash, id_priv)
     - "ecdsa"               : ecdsa_sign(commit_hash, id_priv)  (recoverable; r || s || v)

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 — 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).

Enclave Identity and Collision:

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.

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. Discovery mechanisms (Registry, DNS, well-known URLs, bootstrap config) decide which the client reaches; canonicalization lives outside the core protocol. The Registry enclave's specific rules for handling such collisions are in enclaves/registry.md.

Manifest exp Field:

The exp field in a Manifest commit follows the generic commit rule in §Commit Object: it defines the latest time at which the node is allowed to accept the commit. After enclave creation, the Manifest's exp has no ongoing effect.

Manifest Validation

Validation Order:
  1. Verify commit structure (fields present, types correct)
  2. Verify commit hash and signature
  3. Verify enclave ID derivation matches
  4. Parse and validate content JSON
  5. Apply content-specific rules below

The node MUST reject a Manifest commit if:

  1. enc_v is not a supported protocol version
  2. states is not a non-empty array of UPPER_CASE strings
  3. traits is not an array of name(rank) strings with valid non-negative integer ranks
  4. init is empty or contains invalid entries (each MUST have identity, state, traits[])
  5. Any identity in init is not a valid 32-byte public key
  6. Any State in init is not declared in states
  7. Any trait in init is not declared in traits
  8. Manifest fails any validation rule defined in rbac/manifest.md §Validation Rules

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

FieldValue
typeUpdate
contentThe replacement content
tagsMUST include ["r", "<target_event_id>"]

Semantics

  • The target event MUST be a Content Event (AC events cannot be updated).
  • The target event (referenced by r tag) 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 can be updated multiple times; each Update MUST reference the original event (not the previous Update).
  • The SMT leaf for the original event points to the latest Update.
SMT Update Tracking:

When an Update event is finalized:

  1. Node looks up the target event ID from the r tag
  2. Node writes SMT[target_event_id] = update_event_id
  3. 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.

Empty Content:

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 Sender the U permission for 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 Already-Updated Event:

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

FieldValue
typeDelete
contentJSON object (see below)
tagsMUST 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\":\"...\"}"

Content fields:
FieldRequiredDescription
reasonYes"author" (self-deletion) or "moderator" (admin/role deletion)
noteNoOptional explanation (e.g., "policy violation")

Semantics

  • The target event MUST be a Content Event (AC events cannot be deleted).
  • The target event (referenced by r tag) 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 Sender the D permission for message, only the original author can delete their own messages.

Self Evaluation Example:

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 Already-Updated Event:

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.


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.

TemplateDescription
noneNo template; use explicit schema
v2 Scope:

In ENC v2, 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 enclaves.md.


Migration

Enclave migration (the Migrate event, eager / lazy / fork modes, checkpoint verification, split-brain prevention, backup pattern) is specified in migration.md.