Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Types Reference

crates/types -- Shared Domain Primitives

ID Types

#![allow(unused)]
fn main() {
const CHAT_ID_LEN: usize = 32;
const USER_ID_LEN: usize = 20;
const SENDER_ID_LEN: usize = 20;
const MSG_ID_LEN: usize = 32;

type ChatId = [u8; 32];
type UserId = [u8; 20];
type SenderId = [u8; 20];
type MsgId = [u8; 32];
}

ChatKind

#![allow(unused)]
fn main() {
#[serde(tag = "t", content = "d")]
enum ChatKind {
    #[serde(rename = "0")]
    Dm { peer: UserId },          // Direct message (peer semantics: see below)
    #[serde(rename = "1")]
    Group { title: Option<String> },
    #[serde(rename = "2")]
    Channel { title: Option<String> },
}
}

Methods: type_id() -> u8, is_dm() -> bool Default: Dm { peer: [0u8; 20] } (backward compatibility)

CBOR representation: {"t": "0", "d": {"peer": [...]}} -- compact tagged format.

Two wire-level notes:

  • The tag t is a CBOR text string ("0"/"1"/"2"), never an integer -- a consequence of serde's adjacently-tagged encoding, kept as-is for compatibility with already-stored data.
  • Dm.peer semantics depend on where the value lives. In wire messages (MsgV1.kind, gossip PutMessage.kind) it is the original recipient as fixed by the sender -- identical bytes for every reader, so in a received message it equals the reader's own address (interlocutor = peer == me ? sender : peer). In user_inbox entries (and therefore in the /conversations API) the node rewrites peer per inbox owner to be the other participant (upsert_inbox_for_members in crates/db), so there it is always viewer-relative.

Note: User identity blobs (e.g. public keys) are stored in the dedicated identity CF (see STORAGE.md), not inside ChatKind.

Message Types

There are no msg_type constants in code -- the field is a client-defined opaque u8 (0 = plain message, set by the plain message endpoints). Known control conventions (attachments 10, MLS Welcome/Commit 20/21, retired 11/12/22, app-private 100-255) are documented in the client guide registry. User content never claims a control type -- it rides text, optionally marked with a client format prefix.

HlcTimestamp

#![allow(unused)]
fn main() {
#[serde(transparent)]
struct HlcTimestamp(u64);  // packed: 48 bits physical_ms + 16 bits logical
}

Hybrid Logical Clock timestamp used by CRDT-like domains (members, identity) and as a network-wide ordering key for messages. Packed into a single u64:

  • Upper 48 bits: physical_ms -- milliseconds since UNIX epoch
  • Lower 16 bits: logical -- per-node counter for events within one ms

Methods: from_parts(phys, log), from_packed(u64), to_packed() -> u64, physical_ms() -> u64, logical() -> u16, next_logical() -> HlcTimestamp. Ord is derived from the packed u64, which gives lexicographic (physical, logical) ordering -- the property CRDTs rely on for last-writer-wins decisions.

Constants: MAX_PHYSICAL_MS (48 bits set, year ~10889), MAX_LOGICAL = u16::MAX, HlcTimestamp::ZERO (sentinel for absent optional HLC fields in record-id derivation).

Serde: #[serde(transparent)] -- wire encoding is identical to a raw u64. The 8-byte slot for the legacy ts: u64 in seen_msg value layout, messages CF key, and identity CF value is reused without size change. Big-endian encoding of the packed value preserves HLC ordering under RocksDB's lexicographic key comparison.

Clock

#![allow(unused)]
fn main() {
trait Clock: Send + Sync + 'static {
    fn now_ms(&self) -> u64;
}

struct SystemClock;             // wraps SystemTime::now()
struct MockClock { ... }        // AtomicU64-backed, test-controllable
}

Time-source abstraction. Production uses SystemClock; tests use MockClock with set(ms) and advance(delta_ms). The abstraction exists so HLC's clock-skew behaviour is reproducible in the test suite.

Shared via Arc<dyn Clock> so multiple owners (HLC state, retention loop) can read the same source without further synchronisation.


crates/db -- Storage Types

MsgV1 (stored in CF messages)

#![allow(unused)]
fn main() {
struct MsgV1 {
    schema: u8,                   // Always 1
    msg_id: MsgId,
    chat_id: ChatId,
    sender: SenderId,
    hlc: HlcTimestamp,            // Server-stamped HLC (storage/CRDT/sync)
    origin_wall_ts: u64,          // Frozen originator wall-clock (UI display)
    seq: u32,                     // Monotonic sequence within chat
    text: String,                 // current text (post-edit; empty if deleted)
    msg_type: u8,                 // client-defined; 0 = regular text
    control: Option<Vec<u8>>,     // CBOR payload for control messages
    kind: ChatKind,
    edited_at: Option<HlcTimestamp>, // HLC of the winning Edit (absent = never edited)
    deleted: bool,                // true once deleted (stub row, text cleared)
}
}

edited_at and deleted are the materialized projection of any MessageOp records targeting this message (see PROTOCOL.md). Both are #[serde(default, skip_serializing_if = ...)], so an untouched message serializes byte-for-byte as before -- adding edit/delete support changed no existing wire vector. msg_id is not recomputed on edit: it stays the message's stable identity even though the stored text no longer hashes to it.

ChatMeta (stored in CF chats_meta)

#![allow(unused)]
fn main() {
struct ChatMeta {
    last_ts: u64,
    last_seq: u32,
    last_msg_id: MsgId,
    members: Vec<UserId>,
    last_msg_ts: u64,
    last_announced_ms: u64,
    title: Option<String>,          // Group creation title (immutable)
    creator: Option<UserId>,        // Group creator (immutable)
    create_nonce: Option<Vec<u8>>,  // 16-byte creation nonce (immutable)
}
}

The title / creator / create_nonce trio is the group's immutable creation metadata, written once via set_group_create_meta and verifiable against the chat_id (blake3(domain || creator || nonce || title)). All three are None for DMs; title alone is None for unnamed groups. See STORAGE.md and SYNC.md.

InboxEntry (stored in CF user_inbox)

#![allow(unused)]
fn main() {
struct InboxEntry {
    chat_id: ChatId,
    kind: ChatKind,
    last_ts: u64,
    last_msg_id: MsgId,
    last_sender: SenderId,
    last_seq: u32,
}
// No content preview -- previews are derived client-side.
}

MemberInfo (stored in CF members)

#![allow(unused)]
fn main() {
struct MemberInfo {
    role: u8,                              // 0 = participant, 1 = admin, 2 = owner
    added_at: HlcTimestamp,                // HLC of the latest Add
    removed_at: Option<HlcTimestamp>,      // HLC of the latest Remove
}

impl MemberInfo {
    fn is_active(&self) -> bool {
        match self.removed_at {
            None => true,
            Some(r) => self.added_at > r,
        }
    }
}
}

Public reads (get_member_info, list_members, is_member) filter to is_active(). Sync helpers iterate raw seen_member and include tombstones so Merkle anti-entropy can carry removed_at between peers. See STORAGE.md for the full CRDT model.

MessageOpRecord (stored in CF message_ops)

#![allow(unused)]
fn main() {
struct MessageOpRecord {
    target: MsgId,           // message being changed
    chat_id: ChatId,         // chat the target belongs to
    editor: UserId,          // must equal the target's original sender
    op_kind: u8,             // OP_KIND_EDIT = 0, OP_KIND_DELETE = 1
    new_text: String,        // replacement text (empty for Delete)
    hlc: HlcTimestamp,       // server-stamped by the originating node
    sig: Vec<u8>,            // client sig for a Delete; empty for an Edit
}
}

An append-only edit/delete operation. The same struct is the stored record, the gossip payload, and the anti-entropy wire record -- no re-encode. op_kind == OP_KIND_DELETE is terminal. Only a delete is signed (and re-verified by every node): erasure yields a deleted stub that cannot be authenticated end-to-end, so the node authorizes it. An edit is unsigned -- its content is checked by the recipient on Layer 2, like a plain message. The message row clients read is the materialized fold of a message and all its ops; the fold rule (op_applies_to) and apply_message_op live in crates/db/src/message_ops.rs. See PROTOCOL.md and SYNC.md.

Query/Result Types

#![allow(unused)]
fn main() {
struct PutMsg<'a> {
    chat: &'a ChatId,
    sender: &'a [u8; 20],
    hlc: HlcTimestamp,        // Stored in CF `messages` key + msg_id hash
    origin_wall_ts: u64,      // Frozen sender wall-clock for UI display
    text: &'a str,
    kind: ChatKind,
    msg_type: u8,
    control: Option<&'a [u8]>,
}

struct RangeQuery<'a> {
    chat: &'a ChatId,
    from_ts: u64,
    to_ts: Option<u64>,
    after_key_b64: Option<String>,
    limit: usize,
    reverse: bool,            // false = oldest-first from `from_ts`;
                              // true = newest-first from `to_ts`
}

struct RangePage {
    items: Vec<(String, Vec<u8>)>,        // (key_b64, msg_cbor)
    next_after_key_b64: Option<String>,
}

struct ListChatsQuery<'a> {
    user: &'a [u8; 20],
    limit: Option<usize>,
    after_cursor_b64: Option<String>,
}

struct ListChatsPage {
    items: Vec<InboxEntryWithCursor>,
    next_after_cursor_b64: Option<String>,
}
}

crates/api -- HTTP Types

Command Enum

#![allow(unused)]
fn main() {
enum Command {
    PutMessage {
        chat_id: [u8; 32],
        kind: ChatKind,
        sender: [u8; 20],
        members: Option<Vec<[u8; 20]>>,
        text: String,
        // HLC + origin_wall_ts are stamped server-side in the MPSC
        // handler; clients never specify time.
        msg_type: u8,
        control: Option<Vec<u8>>,
        resp: oneshot::Sender<Result<PutMessageResponseRaw, String>>,
    },
    ListUserChats {
        user: [u8; 20],
        limit: usize,
        after_key: Option<Vec<u8>>,
        resp: oneshot::Sender<Result<ListUserChatsResponseRaw, String>>,
    },
    GetChatRange {
        user: [u8; 20],
        chat_id: [u8; 32],
        from_ts: u64,
        to_ts: Option<u64>,
        after_key: Option<Vec<u8>>,
        limit: usize,
        reverse: bool,        // false = oldest-first; true = newest-first
        skip_membership_check: bool,
        resp: oneshot::Sender<Result<GetChatRangeResponseRaw, String>>,
    },
    GetMessageById {          // Single message by id; node authorizes caller
        user: [u8; 20],
        msg_id: [u8; 32],
        resp: oneshot::Sender<Result<Option<Vec<u8>>, String>>,  // None = 404
    },
    ReadChatMessage {
        user: [u8; 20],
        chat_id: [u8; 32],
        seq: u32,
        resp: oneshot::Sender<Result<(), String>>,
    },
    GetNetworkNodes {         // Catalog of connected API-serving peers
        resp: oneshot::Sender<Result<Vec<NetworkNodeRaw>, String>>,
    },
    MessageOp {               // Edit/delete own message; node loads target,
        target: [u8; 32],     // checks authorship + 48h window, stamps HLC,
        editor: [u8; 20],     // verifies sig, then routes DbOp + gossip
        op_kind: u8,          // 0 = Edit, 1 = Delete
        new_text: String,
        sig: Vec<u8>,
        resp: oneshot::Sender<Result<MessageOpResponseRaw, String>>,
    },
    // Plus: MembershipOp, LeaveGroup, GetGroupMembers, SetIdentity,
    // GetIdentity -- see crates/api/src/command.rs
}
}

Raw Response Types

#![allow(unused)]
fn main() {
struct PutMessageResponseRaw {
    chat_id: [u8; 32],
    msg_id: [u8; 32],
    origin_wall_ts: u64,    // HTTP DTO surfaces this as `ts` for backward compat
}

struct InboxChatRaw {
    chat_id: [u8; 32],
    kind: ChatKind,
    last_ts: u64,
    last_sender: [u8; 20],
    unread: u32,
    cursor_key: Vec<u8>,
    source: String,
}

struct ListUserChatsResponseRaw {
    items: Vec<InboxChatRaw>,
    next_after_key: Option<Vec<u8>>,
}

struct ChatMessageRaw {
    key_raw: Vec<u8>,
    msg_cbor: Vec<u8>,
}

struct MessageOpResponseRaw {
    msg_id: [u8; 32],
    op_ts: u64,             // wall-clock component of the op's HLC ("edited at")
}

struct GetChatRangeResponseRaw {
    items: Vec<ChatMessageRaw>,
    next_after_key: Option<Vec<u8>>,
}

struct NetworkNodeRaw {
    peer_id: String,    // base58 PeerId
    api_url: String,    // advertised HTTP API base URL
}

struct GroupMembersRaw {
    members: Vec<([u8; 20], u8)>,   // (address, role) pairs
    title: Option<String>,          // group creation title from chats_meta
}
}

DTO Types (JSON serialization)

#![allow(unused)]
fn main() {
#[serde(tag = "type")]
enum ChatKindDto {
    #[serde(rename = "dm")]
    Dm { peer: String },
    #[serde(rename = "group")]
    Group { title: Option<String> },
    #[serde(rename = "channel")]
    Channel { title: Option<String> },
}

struct PostDirectMessageBody { text: String }               // validate: 1-45056 bytes
struct PostDirectMessageResponse { chat_id, msg_id, ts }

struct PostControlDirectMessageBody { msg_type: u8, control: String }
// validate: msg_type 1-255, control base64 1-43692 chars (32 KiB decoded)
struct PostControlMessageResponse { chat_id, msg_id, ts }

struct ListUserChatsQueryParams { limit: Option<usize>, after: Option<String> }
// validate: limit 1-1000
struct InboxChat { chat_id, kind, last_ts, last_sender, unread, cursor }
struct ListUserChatsResponse { items: Vec<InboxChat>, next_after: Option<String> }

struct GetChatRangeQueryParams { from, to, after, limit, reverse: Option<bool> }
// validate: limit 1-1000; reverse omitted => false (oldest-first)
struct ChatMessage { key: String, msg_cbor: String }
struct GetChatRangeResponse { items: Vec<ChatMessage>, next_after }
struct GetMessageResponse { msg_id: String, msg_cbor: String }  // GET /messages/{msg_id}

struct ReadChatMessageBody { seq: u32 }                     // validate: >= 1

struct NodeInfoResponse { peer_id: String, api_url: Option<String> }  // GET /node/info
struct NetworkNodeDto { peer_id: String, api_url: String }
struct NetworkNodesResponse { nodes: Vec<NetworkNodeDto> }  // GET /network/nodes

struct CompoundMembershipRequest { ops, messages, nonce: Option<String>, title: Option<String> }
// title: create-only, 1-128 UTF-8 bytes, no control chars; part of the
// chat_id preimage (see PROTOCOL.md)
struct GroupMember { address: String, role: u8 }
struct GroupMembersResponse { members: Vec<GroupMember>, title: Option<String> }
}

crates/node -- Node Types

GossipMessage

See PROTOCOL.md for full specification.

Key structs (abbreviated; full spec in PROTOCOL.md):

#![allow(unused)]
fn main() {
struct PutIdentity {
    user: [u8; 20],     // User address
    blob: Vec<u8>,      // Opaque identity blob (max 1024 bytes)
    hlc: HlcTimestamp,  // Server-stamped HLC, used for LWW
    origin: String,     // PeerId of the originating node
}

// Edit/delete an existing message. Carries `MessageOpRecord` verbatim
// (see crates/db), so gossip and anti-entropy ship identical bytes.
// GossipMessage::MessageOp(MessageOpRecord)
}

DbOp

#![allow(unused)]
fn main() {
enum DbOp {
    // HLC drives storage/CRDT/sync; origin_wall_ts is frozen sender wall-clock for UI.
    PutMessage { chat_id, sender, text, hlc: HlcTimestamp, origin_wall_ts: u64, kind, msg_type, control },
    UpsertInbox { chat_id, kind, users, all_members, ts, seq, msg_id, sender },
    SetReadProgress { user, chat_id, seq },
    // Discrete Add/Remove/Create from API or gossip. `hlc` is server-stamped.
    MembershipOp { chat_id, target, role, op_type, hlc: HlcTimestamp, recovered_signer },
    // Full peer state from anti-entropy sync. CRDT-merged with local record.
    ApplyMemberRecord { chat_id, target, info: MemberInfo },
    // Immutable group creation trio -> chats_meta (write-once). Senders
    // MUST have verified chat_id == blake3(domain||creator||nonce||title).
    SetGroupCreateMeta { chat_id, title: Option<String>, creator, nonce: Vec<u8> },
    // HLC-LWW on identity blob (max 1024 bytes).
    PutIdentity { user, hlc: HlcTimestamp, blob },
    // Message row replicated verbatim from a peer during sync (id +
    // edited_at + deleted preserved). Live authoring uses PutMessage.
    ApplySyncedMessage { msg: Box<MsgV1> },
    // Record an edit/delete op + materialize it into the target row,
    // one WriteBatch. From HTTP, gossip, and sync alike.
    MessageOp { record: MessageOpRecord },
    DeleteInboxEntry { user, chat_id },
    // Atomic two-record role swap: old owner -> admin, new owner -> owner.
    TransferOwnership { chat_id, current_owner, new_owner, hlc: HlcTimestamp },
    // Bulk-tombstone all members + per-user inbox cleanup. Messages left to GC.
    DeleteGroup { chat_id, hlc: HlcTimestamp },
}

/// Wrapper around UnboundedSender<DbOp> that increments
/// db_writer_queue_depth gauge on every successful send.
struct DbOpSender { inner: mpsc::UnboundedSender<DbOp> }

enum MerkleUpdate { Insert([u8; 32]), Replace { old, new }, Remove([u8; 32]) }

struct MerkleSenders {
    msg_tx: UnboundedSender<[u8; 32]>,          // messages (append-only)
    member_tx: UnboundedSender<MerkleUpdate>,    // members (mutable)
    identity_tx: UnboundedSender<MerkleUpdate>,  // identity (mutable)
    op_tx: UnboundedSender<[u8; 32]>,            // message ops (append-only)
}
}

Sync Types

See SYNC.md for SyncDomain, SyncRequest, SyncResponse, SyncState, SyncSession.

Retention Types

See RETENTION.md for the full GC design.

#![allow(unused)]
fn main() {
struct CycleStats {
    removed_count: usize,    // msg_ids removed from seen_msg and the Merkle tree
    chats_processed: usize,  // chats range-deleted in `messages`
    hit_limit: bool,         // true if GC_BATCH_LIMIT was reached
}
}

Constants in crates/node/src/retention.rs: RETENTION_WINDOW, GC_INTERVAL, GC_SHORT_INTERVAL, GC_BATCH_LIMIT, GC_CHUNK_SIZE.

Functions: cutoff_ts_at(now) -> u64, cutoff_ts_now() -> u64, gc_cycle(db, merkle_msgs, now) -> CycleStats, run_gc_loop(db, merkle_msgs, cancel).

HlcState (per-node HLC)

#![allow(unused)]
fn main() {
struct HlcState {
    state: AtomicU64,          // packed HlcTimestamp
    clock: Arc<dyn Clock>,
    max_drift_ms: u64,
}

enum HlcError {
    DriftTooLarge { incoming_ms: u64, local_ms: u64, max_drift_ms: u64 },
}

const DEFAULT_MAX_DRIFT_MS: u64 = 5 * 60 * 1000;  // 5 minutes
}

Per-node Hybrid Logical Clock state. Generates outgoing HLC stamps (stamp()) and merges incoming HLC values from gossip (receive()). Lives in-memory only -- on restart, the state resets to (clock.now_ms(), 0) and self-corrects via the first incoming gossip.

API:

  • HlcState::new(clock) -- default 5-minute drift bound
  • HlcState::with_max_drift(clock, max_drift_ms) -- custom bound (tests)
  • stamp() -> HlcTimestamp -- always strictly greater than every prior stamp from this state; wait-free in the uncontended case
  • receive(remote) -> Result<HlcTimestamp, HlcError> -- advances local state to dominate both. Returns DriftTooLarge and leaves state unchanged if remote.physical_ms > now + max_drift_ms

Shared via Arc<HlcState> across handlers without further synchronisation -- the single AtomicU64 serialises all updates.

NodeHandle

#![allow(unused)]
fn main() {
struct NodeHandle {
    cmd_tx: mpsc::Sender<Command>,
    local_peer_id: PeerId,
    db: Arc<ChatDb>,
    listen_addrs: Arc<RwLock<Vec<Multiaddr>>>,
}
}

Methods: shutdown(self) -- cancel token + await task. Returned by run_node(AppConfig).

HandlerContext

#![allow(unused)]
fn main() {
struct HandlerContext<'a> {
    db: Arc<ChatDb>,
    swarm: &'a mut Swarm<MyBehaviour>,
    pending_queries: &'a mut HashMap<[u8; 16], PendingQuery>,
    peer_cache: &'a PeerCache,
    api_registry: &'a HashMap<PeerId, String>,  // connected peers' advertised API URLs
    local_peer_id: PeerId,
    local_peer_id_str: Arc<str>,   // cached PeerId string (avoids Base58 per request)
    db_write_tx: DbOpSender,
    inbox_batch_tx: Option<InboxBatchSender>,
    hlc: Arc<HlcState>,
}
}

Configuration

#![allow(unused)]
fn main() {
struct AppConfig {
    keypair: Keypair,               // secp256k1
    listen: Multiaddr,              // e.g. /ip4/0.0.0.0/tcp/4001
    bootnodes: Option<Vec<Multiaddr>>,
    listen_api: Option<SocketAddr>, // e.g. 0.0.0.0:3000
    db_path: Option<String>,
    expose_metrics: bool,
    metrics_listen: Option<SocketAddr>,
    sync_interval_secs: u64,        // Merkle sync tick (default 30)
    public_api_url: Option<String>, // advertised API base URL (identify announcement)
}
}

TOML config file format (loaded via config::load_config):

private_key = "0x..."
listen = "/ip4/0.0.0.0/tcp/4001"
bootnodes = ["/ip4/.../tcp/4001/p2p/..."]
listen_api = "0.0.0.0:3000"
db_path = "./chatdb-data"
expose_metrics = true
metrics_listen = "0.0.0.0:9090"
# Publicly reachable base URL of this node's HTTP API. Advertised to
# peers via libp2p identify; omit on relay-only / private nodes.
public_api_url = "https://node1.example.com:8080"

public_api_url is validated on load (http(s):// scheme, no whitespace, non-empty host, <= 256 bytes; trailing / is stripped).