Architecture
One node, one hosted chain tree
Section titled “One node, one hosted chain tree”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/BetaThe 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.
Dependency direction
Section titled “Dependency direction”The node is split into a deterministic core and an I/O shell:
HTTP requests ─┐Ivy messages ──┼─> NodeRuntime ─> NodeCore.step(event) ─> ordered effectstimers/jobs ───┘ │ │ │ ├─> NodeStorage │ ├─> content fetch │ └─> Ivy send └─> published snapshots ─> ChainReads ─> HTTP responsesThe 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.
Deterministic core
Section titled “Deterministic core”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 messagesProduction runtime
Section titled “Production runtime”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:
- Persist the
NodeBatchand referenced content. - Publish the new per-level snapshots and read views.
- 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 locksStorage and recovery
Section titled “Storage and recovery”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 togethervolume-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.
Networking and synchronization
Section titled “Networking and synchronization”The Ivy adapter has three distinct wire responsibilities:
ChainHandshakeestablishes the Nexus identity and chain-root context;ChainSyncWiretranslates decodedSyncMessagevalues to bounded overlay frames for a specific hosted path;OverlayWirecarries 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/.
API and reads
Section titled “API and reads”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.
Hosted children and merged mining
Section titled “Hosted children and merged mining”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.
External mining
Section titled “External mining”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 effectsThe 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