FAQ

How the parts actually work.

These answers are first drafts, written against the current implementation. They are deliberately short; each will grow into a proper page. Select a question to open it.

How does peer discovery work?#

A node starts from what it is told statically — the genesis file, which carries the founding committee's addresses, and an optional preferred-peers list of node-ids it should dial first. preferred-peers holds names, not addresses: the address is resolved through discovery, so a peer that moves is still reachable.

From there nodes gossip what they know about each other on an interval (gossip-interval-seconds, 60 by default). A node advertises two separate things: a transport-address, where peers dial it for the peer-to-peer link, and optionally a peer-url, where peers may fetch content from it over TLS. Advertising neither is a valid configuration — such a node dials out and participates, but cannot be dialled, which is ordinary for anything behind NAT.

Connection budgets are directional: allowed-inbound and allowed-outbound are separate limits, and there is deliberately no single combined cap. A pinned seed peer plus slack in both directions is what lets a new node find the rest of the mesh instead of being stuck with only its seed.

What consensus algorithm do we use?#

Jolteon — a 2-chain HotStuff-style BFT protocol. Blocks are certified by quorum certificates built from BLS12-381 aggregate signatures, so a certificate stays one signature-sized object however large the committee. It tolerates f < n/3 Byzantine faults.

Two things are worth knowing beyond the textbook protocol. It is idle-quiescent: with nothing to order, the chain settles onto a slow heartbeat rather than minting empty blocks forever. And leader reputation sidelines dead or slow leaders from the proposing rotation without lowering the quorum threshold — liveness improves, and the safety margin is untouched.

Committee membership is versioned by block height, so a certificate is validated against the committee as it stood at that round rather than the committee as it is now. Without that, reconfiguring the committee would retroactively invalidate history.

How are transactions distributed?#

A client submits over HTTPS to any node — it does not need to know which node is the leader, or to be connected to the committee at all. The receiving node validates and admits the transaction to a bounded mempool. Bounded is the important word: when it is full the submit is refused with 503 queue_full rather than accepted and quietly dropped, so a saturated node tells you it is saturated.

If that node is not itself on the committee, it forwards the transaction hop-by-hop towards the committee. Peers advertise their quorum index — roughly, their distance from the committee — and a node routes to the connected peer with the lowest index, batching where it can. A node that cannot accept a forwarded transaction says so explicitly rather than swallowing it.

The leader then proposes a block, the committee certifies it, and it commits. Duplicate submission is guarded by a durable transaction-id and time-to-live check that is seeded from chain state at startup, so the guard survives a restart instead of being a fresh empty cache each boot.

What are core ledgers?#

Six ledgers are handler-backed — the node binary itself knows what their transactions mean and derives state from them:

ce.evolver.ledger.identity
identities and their authentication keys
ce.evolver.ledger.quorum
committee membership and weights
ce.evolver.ledger.owners
who owns which ledger, and on what terms
ce.evolver.ledger.groups
named principals and group membership
ce.evolver.ledger.list
ordered list state
ce.evolver.ledger.tickets
tickets

Everything else is a non-core, or application, ledger: the chain orders its transactions and never interprets them. That distinction is what makes the substrate general — core ledgers are the chain's own bookkeeping, and your ledgers are opaque to it.

One implementation detail matters more than it looks. “Is this a core ledger?” is answered from the same handler set that the apply path dispatches on, not from a second hand-written list. A separate list would be free to drift, and then admit-time and apply-time would disagree about what a ledger is.

How does data availability work?#

There are two layers, and they compose. Tiering is the base layer and is always in effect: nodes disseminate and serve application bodies among themselves on a best-effort basis, whatever any individual ledger says.

On top of that, a ledger may overlay a DA policy — a named group and a threshold m. Consensus then refuses to admit a transaction on that ledger until at least m members of that group have attested that they hold its body. That turns availability from a hope into a condition of commitment: if the transaction is in the chain, the attestations existed.

The origin always retains its own copy, which is why m=1 is coherent — the sender's own copy is the guarantee. Leave the policy empty instead and the chain orders only an identifier while you host the bytes yourself, anywhere you like.

Bodies move by pull: a node that wants one asks a peer, and a peer that does not have it asks its own peers, hop-bounded and loop-free, so a body can be sourced through intermediaries rather than only from its origin. A holder that cannot be dialled at all — behind NAT, advertising no address — is asked over the link it opened and pushes the bytes back instead. Without that, content held only behind a firewall would be reachable by nobody.

What is the difference between an application channel and an application ledger?#

There is none — they are two names for the same object. The documentation writes it as “application ledgers (channels)”, and the API route settles it: /api/explorer/channels/{ledgerId} is a channel addressed by ledger id.

Which word appears where is a matter of which aspect is in view. Ledger is the term in the code, the wire API and the database — ledger_id, ledger_write_policy — and it emphasises the ordered, agreed log. Channel is the term in configuration and in the dissemination layer — subscribed-channels, the channel overlay, channel_bodies — and it emphasises the thing you subscribe to and whose bodies get moved around.

So: you claim a ledger, you write transactions to it, and you subscribe to it as a channel. One object, two vocabularies, and the asymmetry is historical rather than meaningful.

How does the content overlay work?#

The content overlay is a content-addressed cache that runs over the node mesh. You give your own node some bytes. It returns their key, which is the SHA-256 of those bytes. Anyone on the mesh can then ask their own node for that key. The key is also the integrity check: every hop recomputes the hash on arrival, so a corrupt or poisoned transfer is never stored under a valid key.

Publishing is local and free. An application POSTs bytes to its own node’s local listener (loopback by default), and the node stores them. Nothing touches the chain, no fee is charged, and no peer is contacted. The overlay starts no network traffic on a publish. Fetching is also free. Content only reaches the chain if your application puts the key into a transaction, for example a chat message that references an attachment. That transaction is an ordinary one, and the chain still sees only the key.

Distribution is pull-only and driven by demand. Content never spreads by being pushed out. It moves when somebody asks for its key. That key reaches other people out-of-band: in a channel record, a message or a URL. When a node is asked for a key it doesn’t hold:

  1. Hints first. The caller may name likely holders (?hint=). The node that started the request fetches directly from up to two of them, over HTTPS to each one’s advertised peer URL. Only the starting node acts on hints. A relay passes them on but never acts on them, so a hostile hint can’t turn a path into a traffic cannon.
  2. Then a bounded, widening search. The node asks 2 connected peers chosen at random. If they miss, it asks 2 more. On the third and final round it asks every remaining peer it is connected to. Each peer that misses asks its own peers in the same way, one hop deeper, up to 6 hops. Each request carries an id and a list of nodes already visited, so loops and duplicate branches are cut off.
  3. Duplicate requests merge. If many callers want the same key on one node at once, they share a single upstream search.
  4. Misses are remembered briefly. A search that truly found nothing is cached as “not found” for 15 seconds, so repeated requests for missing content don’t flood the mesh. A peer that only throttled the request doesn’t count as a miss.

When a holder is found, the bytes stream back along the path the request took. They are verified at each hop, and every node on the way keeps a copy. Popular content therefore moves towards its readers on its own. The exception is a single object larger than 8 MiB, which is relayed without being cached. Large uploads are split at publish time, so this rarely applies.

Large objects are chunked. A POST larger than 8 MiB is split into 4 MiB chunks plus a manifest, and the key you get back is the manifest’s hash. A GET of that key expands it transparently. Each chunk is fetched through the overlay separately, and can come from a different holder. Each chunk is verified before its bytes are sent, and range requests fetch only the chunks they cover. The largest single upload is 256 MiB by default.

Holders that can’t be dialled push. A node behind a firewall advertises no peer URL, so nobody can fetch from it directly. Instead it is asked over the peer link it opened itself, and it uploads the bytes to the node that asked. Bytes are accepted only against a request that node actually sent to that peer. Committee nodes run no content overlay and are never asked.

Pinning: what you publish stays. Anything POSTed to your own node is pinned: it is never evicted and is always available from your node. Content fetched from elsewhere lands unpinned in the cache. To pin something you fetched, POST the same bytes to your own node. The same bytes give the same key, so the cached copy is promoted to pinned rather than stored twice. A peer can never make your node pin anything.

How the cache is managed. Unpinned content sits in an LRU cache with a byte budget (max-cache-bytes, 5 GiB by default). When the budget is exceeded, the least recently accessed unpinned entries are evicted until the cache is back under it. An entry is never evicted while it is being served. The access order survives a restart. Pinned content doesn’t count towards that budget. It is limited only by disk space:

  • A publish is refused with 507 before any byte lands unless the announced size plus 10% fits in free space.
  • A chunked upload is cut off with 507 if free space drops below two chunks during the upload.
  • Incoming pushes are guarded the same way.

A publish is never refused because the cache is full. The cache makes room by evicting instead.

Who may ask. Node-to-node fetches use mutual TLS, so every requester is a node with an on-chain identity and can be held to account. Each peer is limited to 20 requests per second, and a node runs at most 32 fetches at once. A publisher can also limit reads to named identities or groups. Group membership is checked against live chain state, and a refusal is passed back along the path as 403.

The overlay is off by default (ce.evolver.content.enabled). It needs a TLS listener, because the peer route depends on client certificates.

Known gap. The free-disk check runs only on publishes and pushes. An ordinary fetch that lands in the cache checks the cache budget but not free disk. If the disk is already full of pinned content, the fetch fails as if the transfer had broken. The search then ends as “not found” and is remembered as a miss for 15 seconds, when it should be a clear “no space” refusal. Tracked as issue #123.