Protocol boundary
The Lattice library is the normative owner of validation, state transitions,
work accounting, and fork choice. Its spec.md, foundational-architecture.md,
consensus-fork-choice.md, and philosophy.md define protocol behavior. This
document records how lattice-node realizes that boundary.
Chain identity
Section titled “Chain identity”A process hosts one Nexus-rooted tree. Every level has one absolute Nexus-inclusive path:
NexusNexus/PaymentsNexus/Payments/ReceiptsThe root and operator-selected descendants run as independent consensus levels
inside one NodeCore. Paths are immutable setup and signed transaction replay
protection; the shared overlay and HTTP surface use them for routing. Each
level validates its own sparse route and chooses its own canonical projection.
The pinned Nexus genesis CID is:
bafyreiggtg4ezifboyekbf4gxst2jr3mpjqcxsbmopy7w6fp46g4ngpgxaIt contains the deterministic premine transaction for public key
ed01fe416588df6e7fa5213c0d3e430f504bb5203172120c86b874826b55f53bdb7d.
On an empty store, the node constructs it locally, recomputes its CID, and
uses it only for configured root bootstrap. The CID is a trust anchor, never a
peer-admission signature permit. Every transaction in any genesis has empty
signers and signatures. Nexus is admitted only at the configured CID. A child
root is built from its configured spec and the parent’s entering state, then
admitted only with a valid mined directory proof. Ordinary post-genesis
transactions remain signature-strict.
This genesis is a storage cutover. Existing node data is not migrated; remove the old chain directory before starting this version.
Transactions
Section titled “Transactions”HTTP messages carry concrete transaction bodies with their
signatures. A cashew header alone is not a complete transaction payload.
lattice-node binds the concrete body back to its CID before admission, then
Lattice validates the transaction for its signed absolute path, which the host
must serve.
Lattice accepts only the domain-separated lattice-tx-v1 envelope signature;
a signature over the bare body CID is invalid. One body therefore has exactly
one valid signature form per key.
A block pays its reward and fees to its rewardRecipient, a header field the
proof of work covers; a block with none burns them. The recipient is only an
address: process identity is never converted into wallet identity, and the
node never receives a private key.
Work and hierarchy evidence
Section titled “Work and hierarchy evidence”One mined root CID identifies one physical grind. It has one terminal block location per chain and may be projected across exact hierarchy edges. Repeated observations of that root do not create additional work.
Same-chain predecessor connectivity makes a later grind support its ancestors within that chain’s GHOST calculation; it does not move the grind’s terminal location onto an ancestor. The sparse directory proof itself identifies the root grind and its unique terminal location in each traversed chain.
CID spellings are normalized at the content-addressing boundary before they become work keys. Alternate multibase text for the same CID therefore cannot create a second identity. This encoding rule is distinct from branch canonicity: every accepted branch’s eligible work still counts whether or not that branch is the current canonical projection.
A child candidate is delivered with a sparse proof from the mined root to that exact child. Lattice verifies the directory path and derives target-qualified work directly from the proof. The carrier block need not itself be valid, accepted, connected, or canonical: proof-of-work is a physical fact, not a parent-consensus claim. Work influences fork choice only after the child block is accepted and connected. No node-local work floor exists: any filter on work that can reach fork choice would be consensus-relevant, so the chain’s own target is the only work gate.
Parent canonicity never affects work. A node hosts a child only with every ancestor, and the child reads two narrow facts from its co-hosted parent: the set of states produced by fully executed parent blocks, and attributed runs for its directory under spec §9.10. The parent cannot declare the child valid or choose its tip.
Every child block anchors its parentState directly to the parent chain’s
executed-from-genesis set. The continuity link therefore runs from the empty
state to the named state; it is not a link from the child’s predecessor. A
weighed but unexecuted parent block issues no fact. A child block whose required
state is not yet present parks and is retried when its parent level executes
more history. Child genesis follows the same anchor rule.
A run report names a quantity only. The child binds it to its own directory,
the block named by a verified carrier proof, and a grind already credited at
that location. It derives runWork - ownWork under an identity keyed by the
carrier and directory, accepts only strict increases, and never revokes it.
A locally submitted grind is one NodeCore step. The root and every carried
block whose target the hash meets contribute to one path-keyed NodeBatch;
all affected facts and cursors commit together before any new snapshot is
published. A remotely learned carried block follows the same acquisition and
proof path on the shared overlay. A missing proof is requested with the child
header from a peer that streamed it; no peer can provide the parent-state
verdict.
parentState commits the carrier’s prevState. It is not a parent-block
backlink and is never inverted to discover ancestry.
Import and durability
Section titled “Import and durability”All ingress follows one sequence:
acquire -> verify -> produce one path-keyed NodeBatch -> retain complete selected volumes -> atomically commit every affected level and cursor -> publish the resulting level snapshotsThe transaction is the durability boundary. Success means the complete tree
batch is durable; failure publishes none of it and stops the runtime. Live
execution and recovery consume the same immutable facts. Path-keyed facts,
indexes, cursors, incomplete header boundaries, and child proofs share
state.db; content lives in volumes.db.
Network plane
Section titled “Network plane”The process has one Ivy network plane. Sync messages carry an absolute path, and a peer session serves every level both hosts. The overlay exchanges transaction announcements, path-scoped sync messages, and content.
A node joins any chain by first being a node of its parent, then asking the DHT which of the parent’s nodes host the child, as a provider announce and lookup work in IPFS. One process hosts a chain’s whole ancestry on one overlay, so every host of a child is already in its parent’s network and they share one DHT: the hosts of a child announce its rendezvous key there, a joiner looks the key up and dials the hosts it finds. The same rule applies at every depth, the key carrying the full path.
Provider discovery is keyed per child chain: the node periodically announces
each hosted child under its path
(lattice.chain-peers.v1:<nexus genesis>:<path>), and names each in its
session hello as the capability host:<path>; Nexus, which every node
hosts, has neither. It does not publish a provider record per block or
state. A child’s current peers are the sessions whose hello named it or that
its providers include. While a hosted child has no current peer, or when
its verified progress stalls, the node dials a bounded random set of that
chain’s providers. On a child block’s first attempt its bundle is asked of the
capable sessions among its current peers before other capable sessions; nothing
else is routed by them. A Nexus stall retries disconnected bootstrap peers.
Parent facts, run reports and merged-mining candidates never cross a network plane: they pass in-process between co-hosted levels.
The overlay currently requires node protocol version 6; mixed-version peers refuse the session.
Adding to the wire without a flag day
Section titled “Adding to the wire without a flag day”The version changes, and old and new nodes part, only for a change that makes them disagree about what is valid. Everything else is an addition, and old and new nodes keep one network:
- An addition is a new message type (a new overlay topic, a new Ivy message
tag) or a new optional field of the hello. An existing message never changes
its meaning or its encoding. Overlay messages are canonical-exact
(
CanonicalJSONMessage: a decoded message must re-encode to the bytes received), so adding a field to an existing message is not compatible: it is a new message on a new topic, asread-endpoint.response.v2stands besideread-endpoint.response. - The hello’s optional
capabilitiesnames what a node speaks beyond version 6 (a sorted list of strings, absent when empty). The hello is decoded leniently, so a node that predates the field, or any later field, accepts a hello that carries it. - A node sends a new message type only to a peer whose hello advertised it. A peer that advertised nothing is spoken to exactly as before.
- What a node does not know it ignores: an unknown capability, an overlay message of an unknown topic, an Ivy message of an unknown tag. None of them ends the session or counts against the peer.
Ivy releases through 14.0.0 end the session of a peer that sends an unknown Ivy tag; later ones ignore it. That is why the third rule is not optional.
Capabilities in use: volume-bundle, the peer answers a Volume bundle
request.
A level’s weigh log streams header and proof entries by position. Stream pages
carry IDs only. getData and getAncestors return bounded header entries that
contain the canonical block bytes, an optional child index, the credited
ChildBlockProof values for that block, and a genesis spec when needed. The
receiver verifies every header and proof before adding it to its own log.
A child index larger than the node’s maxChildIndexBytes (default 1 MiB) or
a proof larger than maxProofBytes (default 64 KiB) is unavailable on that
node, like content it cannot fetch: never judged, never blamed. These are local
resource limits, not consensus rules.
Durable sync cursors are written in the same tree-wide transaction as the
facts they pass.
Verified child proofs are indexed locally by chain path, child CID, and grind
root in state.db. The index is recovery and serving evidence, not a
second network or consensus authority. A restart refuses unreadable or
misindexed proof rows. When serving a child header, the node attaches the
proofs it has already verified; a receiver missing a streamed proof requests
the corresponding header entry from the peer that advertised it.
Peer content exchange is Volume-native. An announcer names one complete Volume by its root CID and must serve that Volume from the exact authenticated session that made the claim. Each connection must complete a compatible hello before it may request a Volume, including a same-key replacement connection. Entry CIDs, bounded framing, and atomic publication are transport/storage details; node protocol messages never request arbitrary CID selections.
A block’s bundle is the content the protocol’s storeBlock defines for it:
the block’s own Volume, its transactions’, its spec and policy modules, and
the materialized pre-state its validation reads (the prevState root and
every trie node on the paths its actions touch), never its post-state, its
parent or its child blocks. Storing a block records those Volume roots in
volume-bundles.db, a cache beside the stores that nothing else reads and
whose failure costs only speed; for a block with no record the server walks
the same paths over its local content and records what it finds. A content
session asks, on its first miss, for the
bundle of its root from the connected peers that advertised volume-bundle,
and is sent those Volumes with one request; a root with nothing recorded
bundles only itself. A bundled Volume is verified, charged to the session and
credited to its server only when the session’s traversal asks for its root,
exactly as a requested Volume is. A Volume, bundled or requested, that does
not verify (another root, or an entry that does not match its CID) costs the
peer that sent it: Tally records a protocol violation, which lowers its
standing. The Volume is then requested again. A Volume nobody sent (none found,
this node’s own capacity or byte budget, its cancellation) blames no one. A
bundle is an optimisation
and never the only way to a block: it is asked for on a block’s first attempt
only, so a retry is the per-Volume traversal; and a session keeps
volume-bundle only while every bundle it is asked for ends (whole, or “no
bundle”) — one whose answer times out, breaks the stream, carries a Volume
that does not verify, or is cut off is from then on fetched from one Volume at
a time, until it reconnects and says hello again. Whatever a bundle lacks, and everything when
no connected peer advertised bundles, is requested one Volume at a time.
Merged-mining candidates pass in-process. One template job reads an immutable
epoch copy of every hosted level and recursively builds at most one candidate
per directory. A child with an executed tip builds on that tip; a child with
no root builds a genesis from its configured spec and the carrier’s entering
state; a root that is weighed but not yet executed waits. Nested genesis waits
until its parent has executed a block of its own. A sibling built before a
child imports another carried block is ordinary fork input: the child’s fork
choice settles it just as it settles stale blocks in conventional merged
mining. Candidate content is retained with the issued work and becomes durable
only if the submitted grind actually secures that level.
Each candidate-root content session uses the node’s NodeResourcePolicy for
the bytes it may hold, and nothing else. ChainSpec.maxBlockSize remains
chain-selected validity for that chain’s complete block Volume boundary
(excluding the spec and materialized state Volumes). Exceeding a local ceiling
declines or defers acquisition without proving the candidate invalid or
punishing its advertiser.
Hierarchy authorization comes from the host, not from a process key choosing a branch: a child level trusts only the parent level it is co-hosted with, read in-process, and no network peer holds a parent or child role. CAS bytes are non-secret availability and grant no validity: the consumer verifies every CID and the exact Lattice evidence it reads.
Transaction pool
Section titled “Transaction pool”The mempool is operationally first-class but never consensus authority. Transactions are same-chain Volumes: peers advertise a transaction Volume root, the receiver pulls it from that exact session, resolves the transaction, and re-materializes the canonical typed Volume locally. Unrelated peer-supplied members are never retained or relayed. Lattice then validates the transaction against the current state, and the node relays only a newly admitted root. Co-hosted parent and child levels never merge mempools.
The pool holds only transactions Lattice finds ready on the executed tip, funds checked; a nonce ahead of the chain or evidence this node lacks is a retriable refusal, never held. Its rules:
- Claimed once: a first signer’s nonce, each state key written, each deposit and each receipt belong to one pooled transaction.
- Budgeted: an account’s debits across the pool never exceed its balance on the tip, so every pooled transaction executes alongside every other.
- Conflicts go to what pays the miner more: a newcomer displaces what it conflicts with, by claim or by an account’s budget, when the next block, filled in fee-rate order, then carries it and pays the relay floor (at least 1) more. In an uncongested pool that is all of their fees plus the floor. Congested, what it displaces may sit anywhere in the pool and its own fee does not count, only the next block’s; a node with a different backlog may judge the same replacement differently.
- Every tip move re-checks each pooled transaction’s nonce, funds and evidence, and one no longer ready leaves. Policies are judged at admission and by templates, which are handed only what the next block can carry (fee order, skipping what does not fit its bytes, count or state growth, each charged its growth at most); what a template’s policies refuse leaves.
There is no time-based expiry; capacity evicts the lowest fee rate. A fee
rate is fee per weight: the most of a block a transaction takes of its bytes,
state growth or count, in bytes (rpc-api.md, fee pricing).
Transactions confirmed on the new chain leave the pool;
ordinary transactions from removed blocks are reinserted when still valid.
Locally submitted transaction roots survive restart and are revalidated before
becoming visible again. Live pool roots use process-owner VolumeBroker pin
deltas and are unpinned on removal; startup clears that owner before restoring
only durable local submissions. Peer transactions are therefore serveable while
pooled but never become restart authority.
External mining
Section titled “External mining”The node constructs the final nonce-zero template and owns transaction selection, contextual child candidates, target calculation, import, durability, and publication. It never runs a nonce-search loop.
lattice-mining-coordinator fetches work, allocates disjoint ranges, detects a
changed tip, and submits workID + nonce. lattice-miner only searches one
immutable serialized block/range assignment.
The miner-facing API is:
POST /mining/templatesPOST /mining/workTemplate requests may name one reward recipient per absolute chain path. The node partitions those recipients through the hierarchy, carries a child’s block only when it pays the recipient named for that chain, and issues only the final parent template.
There is no template mode or deployment transaction. A merged-mining template may carry a child genesis when that hosted level has no root and has a configured spec; later templates carry ordinary child candidates.
HTTP surface
Section titled “HTTP surface”Two listeners serve two surfaces. The unauthenticated operator adapter binds loopback only and refuses any other bind address; it carries the writes and the operator-only reads:
GET /status (with the template digest)GET /metricsPOST /transactionsPOST /mining/templatesPOST /mining/workThe public read listener (--public-read-port) binds all interfaces and
serves only the bounded, non-mutating read allowlist below; the same routes
are registered on the loopback listener so the two cannot drift. /health
is served from an ungated snapshot and omits the template digest.
GET|HEAD /healthGET /transactions/:cid, /accounts/:ownerGET /api/block/latest, /api/block/:id, /api/block/:id/transactions, /api/block/:id/children, /api/transaction/:cid, /api/state/account/:addr, /api/mempool, /api/peers, /api/chain/info, /api/chain/spec, /api/chain/genesisSee RPC API for DTOs and Architecture for component ownership.
Source: lattice-node/docs/protocol.md at 9abd15b1bb05