Epochs & DAG
Every state-changing event in a space — each delegation, invocation, and revocation, plus the KV operations an invocation performs — is recorded into a per-space, hash-linked DAG of epochs. An epoch is a batch of events committed together; it is serialized as a DAG-CBOR object over (a) the CIDs of its parent epochs and (b) the CIDs of its member events, then hashed — so the epoch's own ID is a Blake3 CID that commits to its entire content and ancestry. Walking the parent → child links of these epochs reconstructs the complete, verifiable history of the space, and the sequence numbers assigned along the way give every event a total order. This ordering is compiled and live (it runs on every /delegate and /invoke); the cross-node exchange of these epochs is the unmounted replication subsystem.
Role
The epoch DAG is the Layer 1 consistency backbone. Authorization is a graph problem — a capability is valid only relative to the delegations that precede it — and the epoch DAG is the per-space append-only log that fixes "what happened, in what order." It is what makes a space's history content-addressed and tamper-evident: change any past event and every descendant epoch CID changes. Concretely, it supplies the (seq, epoch, epoch_seq) ordering that the metadata-db stamps onto each kv_write, which is exactly the key last-writer-wins reads sort on.
Mechanics
What an epoch hashes over
epoch_hash(space, events, parents) (tinycloud-core/src/events/mod.rs) builds and hashes a DAG-CBOR Epoch { parents: Vec<Cid>, events: Vec<OneOrMany> }:
- parents — each prior head epoch's
Hashlifted to a CID with the DAG-CBOR codec0x71(h.to_cid(0x71)). - events — one entry per event:
- a Delegation or Revocation is
OneOrMany::One(hash.to_cid(0x55))(raw codec0x55). - an Invocation is expanded by
hash_inv: the invocation's raw CID, plus — if it carried KVOperations for this space — one DAG-CBOR CID per operation (eachKvWrite { key, value: Cid, metadata }/KvDelete { key, version }hashed individually, value hashes lifted with codec0x71). With ops it becomesOneOrMany::Many([inv_cid, op_cid…]); without,OneOrMany::One(inv_cid).
- a Delegation or Revocation is
The serialized object is hashed with hash(...) (Blake3-256). So an epoch CID commits to its parents and the exact set + content of its events, including each KV mutation — IPLD all the way down.
How epochs are appended (transact)
On each transaction (db.rs:781, transact), for every space touched:
- Find the current heads — epochs with no child in
epoch_order(epoch_order::Column::Child.is_null(),db.rs:906-926). These become the new epoch'sparents(a space can have multiple heads → the structure is a DAG, not a chain). - Find the space's max event
seqand set the new batch'sseq = max + 1(db.rs:890-903,934). - Compute
epoch = epoch_hash(space, events, parents)(db.rs:933). - Persist: an
epochrow{ seq, id: epoch, space }; anepoch_orderrow{ parent, child: epoch, space }per parent (linking the DAG); and anevent_orderrow per event{ event, space, seq, epoch, epoch_seq: index-in-epoch }(db.rs:939-996, inserteddb.rs:999-1025).
The ordering tables
Three SeaORM models in tinycloud-core/src/relationships/ (all keyed by space: SpaceIdWrap):
epoch—{ seq, id: Hash, space }; the DAG nodes.epoch_order—{ parent: Hash, child: Hash, space }; the hash links (edges). A head epoch is one that appears as no row'sparent's child — i.e. has no outgoing child edge.event_order—{ seq, epoch: Hash, epoch_seq, event: Hash, space }; places each event at positionepoch_seqwithin epochepoch, under batch numberseq. This(seq, epoch, epoch_seq)triple is the total order, copied verbatim into everykv_write/kv_deleterow.
Shape
The hashed object (DAG-CBOR, then Blake3-256 → CIDv1):
struct Epoch { parents: Vec<Cid>, events: Vec<OneOrMany> } // OneOrMany = One(Cid) | Many(Vec<Cid>)
// parents: each head epoch hash .to_cid(0x71) (DAG-CBOR)
// delegation/revocation event: hash .to_cid(0x55) (raw)
// invocation event: One(inv.to_cid(0x55)) OR Many([inv.to_cid(0x55), op_cids…])
const CBOR_CODEC: u64 = 0x71; const RAW_CODEC: u64 = 0x55;
The ordering surfaces as the row triple (seq: i64, epoch: Hash, epoch_seq: i64).
Relationships
Orders the capability events (delegation/invocation/revocation) and KV operations that the metadata-db stores; built on the same Blake3 CID hashing (codecs raw 0x55 / DAG-CBOR 0x71) used for content; supplies the (seq, epoch, epoch_seq) ordering that conflict-resolution sorts on for last-writer-wins; rooted per space; designed to be exchanged between nodes by the (unmounted) replication subsystem; the content-addressed history that makes the space tamper-evident (see trust-model).
Example
A space with head epoch E0. An owner submits a tinycloud.kv/put photo.jpg invocation. transact reads heads [E0], sets seq = E0.seq + 1, and computes E1 = epoch_hash(space, [put-invocation], [E0]) — DAG-CBOR over parents:[E0.to_cid(0x71)] and events:[Many([inv.to_cid(0x55), kvwrite_op.to_cid(0x71)])], Blake3-hashed. It writes epoch{E1}, epoch_order{parent:E0, child:E1}, and event_order{event:inv, epoch:E1, epoch_seq:0, seq}. E1 is now the sole head; the kv_write for photo.jpg carries (seq, E1, 0) — the ordering a later read uses to pick the latest value.
Status & drift
Shipped and compiled — events, relationships, and db.rs::transact are all declared in tinycloud-core/src/lib.rs and run on every write. What is not compiled is the cross-node piece: tinycloud-core/src/replication/ (which would gossip these epochs between peers) is not declared in lib.rs. So within a single node the DAG is fully real; multi-node convergence over it is the planned replication work. See replication-and-discovery.
Sources
tinycloud-node:tinycloud-core/src/events/mod.rs(epoch_hash,hash_inv,Epoch/OneOrMany, codecs0x71/0x55),tinycloud-core/src/relationships/epoch_order.rs+event_order.rs(epoch_order/event_ordermodels),tinycloud-core/src/models/epoch.rs,tinycloud-core/src/db.rs:781-1025(transact: heads viaepoch_order.Child.is_null(),seq, epoch/order row construction)