Encode, Encrypt, Enclave — concrete instantiation of FTA. Crypto, events, RBAC, Node API, four applications.
ENC is a protocol for building append-only, cryptographically verifiable data structures. Every enclave is a sequenced event log with RBAC, Merkle proofs, and a single sequencer that orders and finalizes commits.
A log-based, verifiable, sovereign data structure. Maintains an append-only event log, SMT state, and CT proof tree.
Hosts one or more enclaves. Stores data, serves queries, produces proofs, and sequences commits into events.
Controls an identity key (id_priv). Produces signatures, encrypts/decrypts, and submits commits.
Indexes, aggregates, and transforms enclave data for efficient querying. Trusted, not verifiable.
Two query paths exist with fundamentally different trust properties:
| Path | Trust | Verification | Use Case |
|---|---|---|---|
| Enclave | Source of truth | SMT + CT proofs | Disputes, audits, high-value transactions |
| DataView | Convenience | None (trusted) | Routine queries, real-time apps |
DataView receives data via three methods, each requiring a role assignment:
| Method | Permission | Description |
|---|---|---|
| Query | R | DataView queries enclave directly as a member |
| Push | P | Node delivers full events proactively |
| Notify | N | Node delivers lightweight notifications; DV fetches as needed |
Each enclave has a single sequencer — the node that orders and finalizes commits. The node that accepts the Manifest event becomes the sequencer, and its identity key is recorded in the Manifest's sequencer field. Multi-sequencer consensus is out of scope for v1.
The sequencer accepts valid commits, assigns seq and timestamp, signs events with seq_sig, maintains the SMT and CT trees, and delivers P/N to registered recipients.
Five cryptographic axioms underpin every theorem. Each is instantiated with a concrete algorithm and modeled as idealized for formal reasoning.
SHA-256 (FIPS 180-4). Modeled as injective: H(a) = H(b) → a = b.
BIP-340 Schnorr over secp256k1. Deterministic signing with zero auxiliary randomness.
secp256k1 ECDH. Computational Diffie-Hellman assumption on the curve.
HKDF-SHA256. One-way key derivation: derive(k, l) does not reveal k.
XChaCha20Poly1305. IND-CPA + INT-CTAG secure authenticated encryption.
Every hash in ENC routes through H() — SHA-256 over deterministic CBOR encoding (RFC 8949 §4.2). CBOR is used only for hash computation, not transport. The v1 wire format is JSON.
-- H(fields...) = sha256(cborEncode(fields)) noncomputable def H (fields : List CborField) : Hash := sha256 (cborEncode fields) theorem H_injective : ∀ (a b : List CborField), H a = H b → a = b := by intro a b hab have hcbor := sha256_injective _ _ hab exact cborEncode_injective _ _ hcbor
All hash operations use a unique single-byte prefix to prevent cross-domain collisions. Signature contexts use string prefixes for domain separation.
| Prefix | Domain | Usage |
|---|---|---|
0x00 | CT Leaf | RFC 9162 leaf hash |
0x01 | CT Node | RFC 9162 internal node |
0x10 | Commit | Client-generated commit hash |
0x11 | Event | Sequencer-finalized event hash |
0x12 | Enclave ID | Content-addressed enclave identity |
0x20 | SMT Leaf | Sparse Merkle Tree leaf |
0x21 | SMT Node | Sparse Merkle Tree internal node |
| Context | String Prefix | Usage |
|---|---|---|
| STH Signature | "enc:sth:" | 56-byte message → SHA-256 → Schnorr |
| Session Token | "enc:session:" | Message → SHA-256 → Schnorr |
Transport uses JSON. Binary values are hex-encoded: hashes and public keys as 64-char hex (no prefix), signatures as 128-char hex, bitmasks as 0x-prefixed hex. CBOR encoding is used exclusively for hash pre-image computation.
An identity key is a 256-bit secp256k1 public key (id_pub). A client controls an identity by holding (or having authorized access to) the corresponding private key. The FTA formalizes four identity variants with cryptographic strength levels.
inductive Id where | anon -- no key, strength 0 | hold (pub : PubKey) -- controls keypair, strength 1 | share (pub : PubKey) (k : Nat) -- holds shard, strength 0 | attest (pub : PubKey) (sig : Signature) -- proved via sig, strength 2 def Id.strength : Id → Nat | .anon => 0 -- cannot create sessions | .hold _ => 1 -- can sign commits | .share _ _ => 0 -- single shard grants no capability | .attest _ _ => 2 -- proved via signature
Stateless query authentication. A 68-byte token (136 hex chars) derived from the identity key, verifiable in O(1) EC math — no signature verification needed. Maximum lifetime: 7200 seconds (2 hours). Clock skew tolerance: ±60 seconds.
-- Client creates: expires = now + duration -- max 7200 seconds message = "enc:session:" || be32(expires) sig = schnorr(sha256(message), id_priv) r, s = sig[0:32], sig[32:64] session_pub = s · G -- point on secp256k1 token = r || session_pub || be32(expires) -- Node verifies in O(1) EC math: t = sha256(session_pub || seq_pub || enclave) expected = r + sha256(r || from || message) · from check: session_pub == expected
Each session derives a per-node, per-enclave signer key for ECDH. This provides per-enclave key isolation: a compromised signer in Enclave A cannot decrypt messages for Enclave B.
t = sha256(session_pub || seq_pub || enclave) signer_priv = session_priv + t (mod n) signer_pub = session_pub + t · G
A Commit is a client proposal. An Event is the sequencer-endorsed result. A Receipt is the node-signed acknowledgment. The dual-signature chain binds author intent to sequencer ordering.
content_hash = sha256(utf8_bytes(content)) -- raw SHA-256, not H() commit_hash = H(0x10, enclave, from, type, content_hash, exp, tags) sig = schnorr(commit_hash, from_priv) event_hash = H(0x11, timestamp, seq, sequencer, sig) seq_sig = schnorr(event_hash, sequencer_priv) event_id = sha256(seq_sig) -- commits to both enclave_id = H(0x12, from, "Manifest", content_hash, tags)
meta to differentiate.| Field | Type | Description |
|---|---|---|
hash | hex64 | Commitment hash of fields |
enclave | hex64 | Destination enclave |
from | hex64 | Author identity key |
type | string | Event type (schema-scoped) |
content | string | UTF-8 payload (opaque to node) |
exp | uint | Expiration (Unix ms) — also acts as nonce |
tags | array | Node-level metadata (see Tags) |
sig | hex128 | Schnorr signature over hash |
Tags provide node-level processing instructions (not query indexes). Content is opaque to the node; tags are understood.
| Tag | Description |
|---|---|
r | Reference — ["r", "event_id", "context"]. Context: target, reply, quote, thread |
auto-delete | Node removes content after this Unix ms timestamp (trust-based, no SMT update) |
The node adds id, timestamp, sequencer, seq, seq_sig to produce the final event. For all events except Migrate (forced takeover), the sequencer field must match the current sequencer.
theorem terminated_rejects_all (commit : Commit) (t : Timestamp) (dup : Bool) (ctx : AuthContext) (s : Schema) : validateCommit commit t .terminated dup ctx s = .reject .enclaveTerminated theorem duplicate_rejected -- dedup gate ... (hNotExp) (hNotFar) : validateCommit commit t ls true ctx s = .reject .duplicateCommit
The node maintains a set of accepted commit hashes per enclave. Duplicate hashes are rejected. Hashes may be garbage-collected after exp + 60000 ms. Rejected commits are not added to the dedup set. Expiration window limit: MAX_EXP_WINDOW = 3600000 (1 hour). Clock skew tolerance: ±60 seconds.
| Category | Events | Modifies SMT |
|---|---|---|
| AC | Manifest, Grant, Grant_Push, Revoke, Revoke_Self, Move, Force_Move, Transfer_Owner, AC_Bundle | ✓ (RBAC) |
| Mutation | Update, Delete | ✓ (Status) |
| Lifecycle | Pause, Resume, Terminate, Migrate | — |
| Content | Everything else (app-defined, e.g. Chat_Message) | — |
Only Content Events can be Updated or Deleted. AC Events are irreversible state transitions. Update/Delete events cannot themselves be U/D'd.
U and D are logical operations implemented as new events referencing prior events via the r tag. The log remains append-only; originals are never mutated. Event status is tracked in the SMT Event Status namespace.
| SMT Proof Result | Interpretation |
|---|---|
| Non-membership (null) | Active OR never existed (combine with CT proof to distinguish) |
0x00 (1 byte) | Deleted |
| 32-byte ID | Updated to this event_id |
Role-Based Access Control consists of an immutable Schema (fixed at Manifest creation) and mutable State (tracked in the SMT as 256-bit bitmasks). Changing the schema requires creating a new enclave.
| Op | Meaning | Applies to |
|---|---|---|
C | Create — can submit commits | All events |
R | Read — can query events | All events |
U | Update — can update events | Content Events only |
D | Delete — can delete events | Content Events only |
P | Push — receives full events | All events |
N | Notify — receives metadata only | All events |
| Bit | Role | Stored in SMT | Description |
|---|---|---|---|
| 0 | Self | No (dynamic) | Matches event.from — for U/D, matches original author |
| 1 | Owner | Yes | Must be assigned in initial_state |
| 2 | Node | No (dynamic) | Matches current sequencer |
| 3 | Any | No (implicit) | Everyone. Cannot have P or N. |
| 4–31 | Reserved | — | Future protocol use |
| 32+ | Custom | Yes | App-defined, ordered by schema position |
{
"event": "Grant", // event type or "*" wildcard
"role": "Admin", // role name
"ops": ["C"], // permitted operations
"target_roles": ["Member"] // privilege escalation bounds
}theorem revokeSelf_rejects_owner (ctx) (schema) : validateRevokeSelf ctx schema "Owner" = false theorem forceMove_protects_owner_from (ctx) (schema) (fromBm toBm actual) (h : fromBm.hasBit bitOwner = true) : validateForceMove ctx schema fromBm toBm actual = false theorem transfer_requires_owner (ctx) (h : ctx.actorBitmask.isOwner = false) : validateTransferOwner ctx = false
| Type | Effect | Content |
|---|---|---|
Grant | Grant role to identity (pull) | { role, identity } |
Grant_Push | Grant role + register webhook | { role, identity, endpoint } |
Revoke | Revoke role from identity | { role, identity } |
Revoke_Self | Remove own role (not Owner) | { role } |
Move | Atomically change bitmask | { identity, from, to } |
Force_Move | Bypass target_roles (no Owner) | { identity, from, to } |
Transfer_Owner | Atomic ownership transfer | { new_owner } |
AC_Bundle | Atomic batch of AC ops | { operations: [...] } |
Grant and Grant_Push are idempotent. Revoke and Grant for roles already held/not held succeed as no-ops. AC_Bundle uses simulated sequential validation: each operation is authorized against state after all preceding ops. If any fails, the entire bundle is rejected.
Push delivers full events with at-least-once semantics (exponential backoff: 1s → 2x → max 5min, 10 retries). Notify delivers metadata only with best-effort. Each delivery is independent; per-type sequence counters (type_seq, 1-indexed, never resets) enable gap detection.
| State | Condition | Accepts | Reads |
|---|---|---|---|
| Active | No Terminate/Migrate; not paused | All (per RBAC) | Yes |
| Paused | Last lifecycle = Pause | Resume, Terminate, Migrate | Yes |
| Terminated | Terminate exists | None | Until deleted |
| Migrated | Migrate exists | None | No obligation |
Terminate and Migrate are mutually exclusive terminal states. The first terminal event wins. Lifecycle is derived from the event log, not stored in SMT.
A single Sparse Merkle Tree stores both RBAC state and event status. Two namespaces, 168-bit depth (21-byte keys), same collision safety as Ethereum addresses (~10⁻³⁰ at 1B entries).
| Property | Value |
|---|---|
| Total depth | 168 bits (21 bytes) |
| Namespace prefix | 8 bits (1 byte) |
| Entry path | 160 bits (20 bytes) |
| Hash function | SHA-256 |
| Empty value | sha256("") = 0xe3b0c44... |
| Update cost | 168 hashes ≈ 170μs |
| Throughput | ~6,000 updates/sec |
-- RBAC namespace (0x00): path = 0x00 || sha256(id_pub)[0:160 bits] -- 21 bytes -- Event Status namespace (0x01): path = 0x01 || sha256(event_id)[0:160 bits] -- 21 bytes
| Namespace | Value | Meaning |
|---|---|---|
| RBAC | 256-bit bitmask (32 bytes big-endian) | Role assignments |
| RBAC | (not in tree) | No roles (zero bitmask → leaf removed) |
| Event Status | (not in tree) | Active |
| Event Status | 0x00 (1 byte) | Deleted |
| Event Status | 32-byte event_id | Updated to this event |
leaf_hash = H(0x20, key, value) node_hash = H(0x21, left_child, right_child) empty_hash = sha256("") -- hardcoded constant
The SMT root (state_hash) is bound to the event log via CT leaf hashes. With bundling, state_hash is recorded per bundle. Two query modes: verified (CT-proven, may be stale) and current (fresh, no proof).
Two Merkle trees provide verifiable state and history. The CT root alone commits to the entire event log AND current state in a single 32-byte hash.
168-bit depth. RBAC (identity → bitmask) + Event Status (event → status). O(168) hashes per operation.
RFC 9162 append-only tree over bundles. Inclusion and consistency proofs detect forks.
bundle = {
events: [event_id_0, ..., event_id_N], -- ordered by seq
state_hash: SMT root after last event
}
events_root:
N=1 → event_ids[0] -- no tree needed
N>1 → binary Merkle tree, internal = H(0x01, L, R)
right-pad with last event_id to power of 2
leaf_hash = H(0x00, events_root, state_hash)
-- CT tree: bundles are leaves, RFC 9162 §2.1
-- Right-pad with last bundle's leaf_hash to power of 2message = "enc:sth:" || be64(t) || be64(ts) || r -- 8+8+8+32 = 56 bytes sig = schnorr(sha256(message), seq_priv) -- Wire format: { "t": 1706000000000, "ts": 1000, "r": "<hex64>", "sig": "<hex128>" }
{
"k": "<hex42>", // 21-byte SMT key
"v": "<hex|null>", // leaf value (null = non-membership)
"b": "<hex42>", // 168-bit bitmap (which siblings present)
"s": ["<hex64>"] // non-empty sibling hashes only
}
// Verify: compute leaf → walk 167..0 using bitmap → compare rootSet in Manifest (immutable). Close when size events accumulated OR timeout ms passed (whichever first). Timeout uses event timestamps, not wall clock. Default: size=256, timeout=5000. Set size=1 for per-event state verification.
Migration transfers an enclave to a new node while preserving the full event log and enclave identity. Only Owner can issue Migrate. Two modes:
Old node online. Owner submits Migrate → old node finalizes as last event → transfers log → new node verifies ct_root → continues sequencing.
Old node offline. Owner signs unfinalized Migrate → submits log + commit to new node → new node replays entire log → verifies everything → finalizes Migrate itself.
{
"new_sequencer": "<new_seq_pub>",
"prev_seq": 1234,
"ct_root": "<ct_root_at_prev_seq>"
}Forced takeover verification: (1) replay all events computing SMT state, (2) verify SMT root matches state_hash in final bundle, (3) verify Migrate sender has Owner bit, (4) recompute CT root — must match ct_root, (5) verify commit signature, (6) verify prev_seq matches last event's seq.
Migration barrier: Once accepted, Migrate is the final event. The node rejects all pending commits, closes the bundle immediately. The ct_root checkpoint proves both log and state.
Backup pattern: To enable forced takeover, ensure someone has the full log — either Owner stores locally, or assign a Backup role with {"event": "*", "role": "Backup", "ops": ["P"]}.
Two layers: transport encryption (always on, ECDH + XChaCha20Poly1305) and content encryption (application-chosen middleware). The node is a blind relay for encrypted content.
| Context | Client Key | Node Key | HKDF Label |
|---|---|---|---|
| Query request | signer_priv | seq_pub | "enc:query" |
| Query response | signer_priv | seq_pub | "enc:response" |
| Push delivery | to (recipient) | seq_priv | "enc:push" |
Nonce: random 24 bytes, prepended to ciphertext. Wire format: nonce || ciphertext || tag (min 40 bytes).
| Scheme | Transport | Evolution | Use Case |
|---|---|---|---|
Plaintext | Direct | Static | Public timeline, registry |
DH_Session | Direct | Epoch | DM conversations |
Group_Lazy | Tree N | Epoch (24h rotation) | Casual group chat |
Group_Secure | Tree N | Ratchet | High-security groups |
theorem dm_roundtrip (senderPriv recipientPriv : ByteVec 32) (senderPub recipientPub : PubKey) (hSender : senderPub = derivePubKey senderPriv) (hRecipient : recipientPub = derivePubKey recipientPriv) (plaintext : ByteArray) : decryptFromSender recipientPriv senderPub (encryptForRecipient senderPriv recipientPub plaintext) = some plaintext
All communication uses a single endpoint (POST /) with request type detection, plus proof retrieval endpoints. Content-type: application/json.
| Method | Path | Auth | Description |
|---|---|---|---|
POST | / | Sig | Submit commit (detected by exp field) |
POST | / | Session | Query events (detected by type: "Query") |
POST | / | Session | Pull missed batches (detected by type: "Pull") |
WS | / | Session | Real-time subscriptions |
GET | /:enc/sth | Public | Signed Tree Head |
GET | /:enc/consistency | Public | CT consistency proof |
POST | /inclusion | R | CT inclusion proof |
POST | /bundle | R | Bundle membership proof |
POST | /state | R | SMT state proof |
First valid Query creates a subscription (node assigns sub_id). Events stream as encrypted messages. One connection supports multiple identities, each with their own session.
| Direction | Type | Description |
|---|---|---|
| C → N | Query | Subscribe (first valid Query) |
| C ← N | Event | Stored + live events (encrypted) |
| C ← N | EOSE | End of stored events marker |
| C → N | Commit | Write event → Receipt |
| C → N | Close | Unsubscribe from sub_id |
| C ← N | Closed | Subscription terminated (with reason) |
{
"id": "<hex64> | [...]",
"seq": "<uint> | [...] | Range",
"type": "<string> | [...]",
"from": "<hex64> | [...]",
"tags": { "r": "abc..." }, // tag filter
"timestamp": { "start_at": N, "end_before": M },
"limit": 100, // max 1000
"reverse": false
}Node aggregates events into a single queue per (identity, url) pair across all enclaves. One push_seq counter for gap detection. Pull fallback via POST / with type: "Pull".
{
"push_seq": 130,
"push": { "enclaves": [{ "enclave": "...", "events": [...] }] },
"notify": { "enclaves": [{ "enclave": "...", "seq": 150 }] }
}| Code | HTTP | Description |
|---|---|---|
INVALID_COMMIT | 400 | Malformed commit structure |
INVALID_HASH | 400 | Hash doesn't match CBOR encoding |
INVALID_SIGNATURE | 400 | Signature verification failed |
EXPIRED | 400 | Commit exp < current time |
DUPLICATE | 409 | Commit hash already processed |
UNAUTHORIZED | 403 | Insufficient RBAC permissions |
ENCLAVE_PAUSED | 403 | Only Resume/Terminate/Migrate accepted |
ENCLAVE_TERMINATED | 410 | Enclave is terminated |
ENCLAVE_MIGRATED | 410 | Enclave has migrated |
SESSION_EXPIRED | 401 | Session token expired |
BITMASK_MISMATCH | 400 | Move/Force_Move from ≠ current state |
OWNER_SELF_REVOKE_FORBIDDEN | 400 | Cannot self-revoke Owner |
OWNER_BIT_PROTECTED | 400 | Force_Move cannot touch Owner bit |
The Registry is an enclave with a DataView server providing discovery. Three event types: Reg_Node, Reg_Enclave, Reg_Identity — all Content Events within the Registry enclave, updatable and deletable.
| Endpoint | Returns |
|---|---|
GET /nodes/:seq_pub | Node endpoints, protocols, enc_v |
GET /enclaves/:enclave_id | Enclave metadata, sequencer, creator |
GET /resolve/:enclave_id | Combined enclave + node lookup |
GET /identity/:id_pub | Identity's registered enclaves map |
Reg_Node: Signed by seq_pub. Contains endpoints (max 10) with priority, protocols, enc_v. Reg_Enclave: Contains the full finalized Manifest event. For post-Transfer_Owner updates, includes owner_proof (STH + CT proof + SMT proof of Owner bit). Reg_Identity: Maps id_pub → enclaves (max 256 entries).
New submissions supersede existing entries for the same key (implicit Update in SMT). First Reg_Enclave for an enclave_id wins; conflicts are errors, not updates.
ENC PROTOCOL · FTA v2.3 · 48 THEOREMS · 162 LEAN PROOFS · 0 SORRY · 63 FILES · 14,627 LINES