Skip to content

Architecture

One lattice-node process hosts a Nexus-rooted tree. Nexus is always the runtime root; hostedChildren adds the child paths that this operator chooses to serve. A nested child is valid only when every ancestor is hosted by the same process.

lattice-node
Nexus
Nexus/Alpha
Nexus/Alpha/Beta

The process has one identity, one Ivy overlay, one RPC listener, one content store, and one serial runtime loop. Each hosted path still has independent consensus state, fork choice, sync state, mempool, and a path-keyed stream in the node’s fact journal. Messages, transactions, reads, and mining work identify the path they concern.

The node is split into a deterministic core and an I/O shell:

HTTP requests ─┐
Ivy messages ──┼─> NodeRuntime ─> NodeCore.step(event) ─> ordered effects
timers/jobs ───┘ │ │
│ ├─> NodeStorage
│ ├─> content fetch
│ └─> Ivy send
└─> published snapshots ─> ChainReads ─> HTTP responses

The dependency points inward. LatticeNodeCore knows consensus values and plain events, but it does not know SQLite, Ivy, HTTP, files, clocks, or tasks. LatticeNode owns adapters and executes the core’s effects. The daemon only maps HTTP requests and responses.

ChainCore is the state machine for one chain path. It owns:

  • header synchronization and fork choice;
  • body acquisition and execution scheduling;
  • child-proof admission;
  • the mempool and mining-template state;
  • the immutable snapshot published to readers.

NodeCore owns one ChainCore per hosted path. Its synchronous step method routes a NodeEvent to the right level, coordinates parent/child facts, and returns ordered NodeEffect values. A mined subtree is handled in one node step, so all contributing levels agree on what the submitted grind changed.

Time is an input to step; work such as execution and proof verification is represented by a job effect whose result returns as another event. This makes the same core usable by production and deterministic simulation.

The core source is grouped by responsibility:

LatticeNodeCore/
Chain/ ChainCore, selection, bodies, child proofs
Host/ NodeCore and multi-level coordination
Mempool/ transaction-pool policy
Mining/ mining state and issued work
Sync/ header sync and decoded sync messages

NodeRuntime is the production shell around NodeCore. One serial task owns the core and is the only code that calls step. Network messages, RPC writes, timer firings, content arrivals, and worker results all enter that task as plain inputs.

For each turn the runtime preserves the core’s effect order:

  1. Persist the NodeBatch and referenced content.
  2. Publish the new per-level snapshots and read views.
  3. Execute sends, fetches, jobs, timers, disconnects, and RPC replies.

Network ingress is bounded before it reaches the loop. Worker tasks never mutate core state; they post their results back as events. A persistence failure is fail-stop because publishing state that was not durably recorded would make restart behavior disagree with the live process.

The production source is grouped by adapter boundary:

LatticeNode/
API/ read operations; the request/response models are LatticeNodeAPI
Configuration/ immutable process configuration and genesis policy
Content/ content-addressed decoding and Ivy content adapters
Mining/ template assembly and multi-level mining plans
Networking/ handshake, overlay frames, sync codec, Ivy delegate
Observability/ metrics and sync tracing
Runtime/ NodeRuntime and storage-backed effect execution
Storage/ SQLite facts, indexes, boot recovery, and locks

NodeStorage is the runtime’s storage gateway. It owns one tree-wide NodeStore, the shared DiskBroker, retained-root scopes, and the durable local mempool journal. Facts, accepted-block indexes, and stream cursors carry their absolute chain path.

Weighed header material and credited child proofs live in state.db beside the facts that reference them. A sync header can arrive before the full block Volume, so this data cannot be inserted into the immutable Volume store as though it were a complete boundary.

<storage>/
state.db facts, headers, child proofs, indexes, cursors, local mempool
volumes.db shared materialized content and retained roots
volume-bundles.db cache: per block, the Volume roots served together

volume-bundles.db is a cache and no part of the recovery unit: nothing but bundle serving reads it, a node runs without it, and it may be deleted while the node is stopped.

Persistence is content-first: referenced Volumes become durable before the fact transaction that names them. One admission’s Volumes (a block’s once its execution found it valid or it was mined here, or the post-states of one step) are one volumes.db transaction, then retained. The complete NodeBatch then commits in one SQLite transaction across every affected chain path, including each path’s headers, child proofs, and sync cursors. A crash cannot leave part of a block’s content or a parent-level half of a multi-level grind.

Content stays in volumes.db because a retention scope names it, and each scope says why. A block’s body (its own Volume and its transactions’) is named by that block on its chain, so a transaction two blocks carry stays until both are let go of. What executions read and create (state, specs, policy modules) and the Nexus genesis block are named by the chain. A body is let go of when its block is found invalid, and, when the operator keeps no bodies beyond the ones the node operates on (blockBodies: none), when another block becomes its chain’s tip. When the operator keeps only the tip’s state (state: tip), state is named by its chain’s state scope, and a node of it is let go of when no node of the tip’s state names it. On tip and on none, what a block reads is named by that block until it executes; on none the state scope names only what the last execution touched. Pins are in the store and a start leaves them as it finds them, and sweeps what no scope names. The journal still lists the roots each batch stored, and nothing reads that list.

A fact does not depend on its content being held, so a store that lacks content its facts name is not damaged.

  • Executing a block reads what its body fetch brought: the block and the state it touches come from this store or from peers. The parent’s own node comes from its header record when its body is not held.
  • Building a template, and storing a mined block and the child blocks it carries, read this store, then a block’s header record, then peers. What a read asked of peers is remembered in memory, within the operator’s budget for one fetch, so a template’s reads can serve the block mined on it. A peer chooses what else it sends with a Volume: none of that is remembered, and nothing a peer sent is written to the Volume store as it came. The store’s Volumes are only the ones this node derives when it stores a block, and a block a peer sent is stored only once its execution has found it valid: until then its body is read into memory, and read again whole from peers if memory let it go.
  • Checking a submitted transaction reads this store alone, and so do the public read routes: anyone may submit or read, and neither spends this node’s requests to its peers. A transaction whose state is not held stays pooled, unchecked.

Every entry is checked against its CID. A read nobody can serve is no verdict: a block parks and is asked for again, and a template is not built. Boot retains the journal’s roots the store holds and does not ask that it hold them all; it stores the Nexus genesis block again when the store lacks it, since nothing else can supply that block’s own node. Retaining a root that has gone since it was fetched retains the rest.

BootRecovery validates the databases before networking starts. NodeRuntime.boot then rebuilds every ChainCore from one ordered scan grouped by path. Saved child proofs must decode and match their indexes; each commits in the transaction of the work fact it produced.

There are no storage migrations. state.db and volumes.db are one recovery unit and must be wiped together on a schema cutover.

The Ivy adapter has three distinct wire responsibilities:

  • ChainHandshake establishes the Nexus identity and chain-root context;
  • ChainSyncWire translates decoded SyncMessage values to bounded overlay frames for a specific hosted path;
  • OverlayWire carries process-level announcements such as transaction availability.

Wire validation and canonical decoding happen at the adapter boundary. Policy decisions remain in NodeCore and ChainCore. The sync state itself is split into HeaderSync, BodyPipeline, and ChildProofSync so a header, its body, and the work proving a child block are not conflated.

A child header is not a second kind of block. It is an ordinary block header for a child path plus one or more ChildBlockProof values showing which ancestor grind contributed work to it. That is why the former ChildHeaders.swift is now ChildProofs.swift: the file owns proof admission and verification, while ordinary header synchronization stays in Sync/.

The HTTP surface has one unversioned set of routes. The daemon decodes a request, calls NodeRuntime for mutations or ChainReads for reads, and encodes the returned API model. There is no second service actor and no version-routing layer.

RPC mutations are core events with reply IDs:

  • submit a transaction;
  • request a mining template;
  • submit mined work.

Reads do not enter the serial loop. NodeRuntime publishes immutable ChainSnapshot and NodeReadView values; ChainReads combines those with content-verified reads from NodeStorage. Each hosted child has its own ChainReads instance. A request for a path this node does not host is a 404.

Every hosted child has its own ChainCore. A parent template may carry the latest candidate from each direct child, and that candidate may recursively carry its own child. The template digest includes every hosted level, so a change to what a child’s next block would carry, a new child candidate, or a child block makes existing miner work stale.

A child candidate commits its carrier’s entering state as its parentState, and Lattice requires a child’s anchor to move only forward along the parent’s history (spec §5.3 step 6). After a parent reorganization the child’s act-on tip may be anchored on the abandoned parent branch, where no candidate on the canonical parent can extend it. The template then builds on the heaviest executed child tip whose anchor the entering state advances from (NodeRuntime.anchoredTip), carrying neither the pool, which stays on the act-on tip, nor children. Honest work therefore moves to that branch, and the child reorganizes with its parent once the branch is heavier.

A nested child’s first block can be built only after its parent has executed a block of its own. Consensus binds the child genesis to the parent’s state. If the parent pays no block rewards and otherwise never changes state, it cannot produce the distinct state needed to host a nested child. Operators must fund state progression on a parent chain before expecting a nested child to start.

When work is submitted, NodeStorage stores only child blocks for which the grind’s verified proof contributes work. NodeCore then weighs the root and all qualifying carried children in path order.

lattice-mining-coordinator
│ POST /mining/templates
▼
lattice-node
│ immutable candidate + search target
▼
lattice-miner workers
│ nonce result
▼
lattice-mining-coordinator
│ POST /mining/work
▼
lattice-node runtime and durable effects

The node owns chain truth, template construction, and submission validation. The coordinator owns work lifecycle and range allocation. Workers only search nonces for an immutable assignment.


Source: lattice-node/docs/architecture.md at 9abd15b1bb05