The consensus plane of the kotoba stack. Ordering, state, execution.
Implements ADR-2608038000
(com-junkawasaki/root).
repo 語 what it is engi縁起 the ENGI/EN mutual-credit currency. inga's first consumer, and where inga's consensus code is being extracted from. innen因縁 a sourced dependency record over human history (entities, contracts, events). Nothing to do with consensus. inga因果 this repo — the consensus plane. Not a currency. Not a history record.
ingais one letter fromengiand one concept frominnen, and inwest.ymlingaandinnenland on adjacent lines. The collision is known and accepted (superproject ADR-2608038000 D5, same treatment askotobase/kotobase-client/kotoba-clientin ADR-2607050900) — this table is the mitigation, so it is required to stay at the top of all three READMEs.
The structure is literally causal: a source chain is a causal history, a committed state root is a causal cut, and validation asks whether this effect followed from that cause.
| ns | what it owns |
|---|---|
inga.head |
the head record a quorum certifies — kotobase.head/v1 with issuer/sig widened to a quorum certificate |
inga.ref |
a kotobase.storage.core/IRefStore whose compare-and-set is decided by the quorum, declaring :linearizable-ref |
inga.state |
F1 — the committed state root as a real CID, over arrangement's 4 content-addressed indices; plus the :cid / :opaque distinction that gates what may back a kotobase ref |
inga.fuel |
F2 — metered execution where running out is a state transition, never an exception |
inga.power |
F3 — the power table as committed state, and a :storage role on the existing bond market |
inga.retrieval |
F3 — crediting that :storage role by asking a witness to produce blocks it claims to hold |
inga.commitment |
a head record proved by the consensus that actually ran — this record is in this block, and this block carries a quorum certificate |
inga.parity |
one scenario over the pure namespaces, run on JVM and nbb, printing one digest |
Pure .cljc. No I/O, no crypto, no wall-clock — signature verification and
the quorum itself are injected, the same seam kotobase.storage.signed-head
uses for sign-fn and engi.consensus uses for hash-fn. The only
dependency is the storage contract it implements, which is itself
zero-dependency.
inga.ref asks inga.head/verify-cert for signatures over
head/canonical-bytes of a head record. This repository's witnesses sign
inga.attest/vote-payload — chain, view, height, block hash, witness.
Those are different byte strings, so no placement of a quorum certificate
satisfies that verifier, and it is why inga.ref's own docstring could say
"the adapter was correct and connected to nothing": the only thing that ever
implemented propose! was a cooperative oracle in the tests. Superproject
ADR-2608198200 has the measurement.
inga.commitment proves membership instead: the record is among the block's
proposals, the block hash is the one the certificate names, and the
certificate verifies. Three checks, each removing a specific attack, and the
binding is checked before the signatures for the same reason
verify-certificate checks membership before the threshold.
It is a separate namespace on purpose. Not in inga.head, which would
leave one namespace owning two proof models with no way to tell from a call
site which one a deployment relies on. Not in inga.ref, which is an adapter
and would then contain the verification it is meant to be given. The head-record
certificate is untouched — a deployment whose witnesses sign head records keeps
working exactly as before.
inga.ref takes head-of! for this: one seam that reads and verifies, because
a commitment proof travels with the read and splitting them would leave the
verifier needing something it was never handed.
A 2f+1 quorum certificate IS a conditional write.
Two writers starting from the same observed head both ask the quorum to
certify sequence n+1. The quorum certifies at most one, so at most one
publishes. No UPDATE … WHERE sequence = ?, no onlyIf.etagMatches, no
If-Match — no host primitive at all.
That matters because the host primitive is exactly what was not portable.
Backblaze B2 has no conditional put on either API, IPNS publishes
unconditionally, and a content-addressed or erasure-coded network has nothing
to be conditional about. kotobase.storage.signed-head documented this
honestly, declared :single-writer-ref, and ended with:
a correct deployment still puts one writer in front of it — a Durable Object, an actor, a lease.
inga is that one writer, replaced by a quorum. Same record shape, profile
raised to :linearizable-ref, and the object store underneath demoted to what
it is actually good at.
(require '[kotobase.storage.core :as storage]
'[inga.ref :as iref])
(storage/compose
{:blocks <any immutable CID-addressed store> ; B2 / R2 / S3 / IPFS / annex
:refs (iref/ref-store
{:read-head! (fn [ref-name] ...) ; dumb read
:write-head! (fn [ref-name head] ...) ; dumb UNCONDITIONAL write
:propose! (fn [record] ...) ; -> {:certified? :cert :current}
:verify-fn (fn [bytes sig witness] ...)
:quorum 3})}) ; 2f+1 for n=3f+1The block half needs #{:immutable-blocks :cid-addressed-read} and nothing
else. compose takes the ref profile from :refs alone, so a block store
that happens to implement IRefStore cannot lend its claim to the
composition — the separation is held by the type, not by convention.
This is what makes ADR-2608039000 (blockchain / 分散型経路に D1 を前提に しない) implementable rather than aspirational.
kbb -M:test # 309 tests, 1,419 assertions
kbb -M:lint # 0 errors, 9 warnings (all pre-existing, in test/)
kbb -M:parity # and the same on nbb -- both must print one line
kbb --backend sci --classpath "src:$(kbb -Spath | tr ':' '\n' | grep kotobase-storage)" \
-e "(require '[inga.parity :as p]) (p/report)"A JVM suite is not evidence about ClojureScript, and ClojureScript is the
runtime kotobase deploys on. inga.parity runs head / fuel / power /
ref on both and checks one digest:
head:70/3/false/false/false fuel:10,/3,3,6/0,0 power:2/1/1/1/4 ref:true/false,cid-1/true,1/cid-2
inga.state is not in the parity digest — arrangement/commit! returns a CID
on the JVM and a js/Promise on cljs, so there is no single synchronous value
to compare. It is covered instead by the full cljs suite:
npm run test:cljs # shadow-cljs :node-test -- 247 tests, 1204 assertionsBoth runtimes run the same 247 tests. That includes every namespace that came from engi, which had no cljs suite for them.
Two things only the cljs build could find, both while the JVM suite was green:
- nbb cannot run
inga.stateat all — SCI dies withProtocol not found: IEquivinside a transitive dependency. shadow-cljs is the compiler kotobase actually deploys with, so that is the vehicle; nbb still runsinga.parity. - arrangement's platform split is not only in what it returns. On cljs it
also expects
blind-fn/encrypt-fn/decrypt-fnto return Promises. Passing the JVM-shaped synchronous seams fails inside arrangement with.then is not a function.
Where this lands in production: inga.replica/state-root is reporting, not
a consensus decision — no adopt or commit path reads it — so a Promise there is
a caller's await, not a protocol break.
The acceptance test is kotobase's own conformance suite, not one written here:
(contract/verify (storage/compose {:blocks (memory/memory-store) :refs refs})
check)
;; => {:profile :linearizable-ref :concurrency :verified}:concurrency :verified means the suite ran its concurrent half — four real
JVM threads racing the same expected head — and found exactly one winner, with
every loser observing that winner.
inga.ref's propose! is injected, and for a while the only thing that
implemented it was the cooperative oracle in its own tests — the adapter was
correct and connected to nothing.
What consensus gives a ref is not a round trip. It gives a total order,
and a total order already decides the question: for each [ref seq] the
first record in the committed prefix wins and every later one loses. There
is nothing to vote on separately. So inga.ref/project + outcome are the
pure half, and the waiting stays in the host, because
-compare-and-set-ref! is synchronous and a commit is not:
submit the record → await the block that carries it → (outcome projection record)
two-writers-race-a-sequence-and-the-commit-rule-decides runs that on the
real replica network: two writers propose different cids at sequence 0,
neither refusable on shape, and only the order can separate them. Every
replica projects the same winner, exactly one writer is certified, and the
loser is told which head actually holds its sequence — a caller that only
learns false retries against the same base forever.
First-wins, not last-wins, is the whole compare-and-set: last-wins would let a writer that lost the ordering overwrite the winner by proposing again.
The conformance suite's quorum is still an oracle. Safety — that two
conflicting certificates at the same height can never both form — is a
kotobase.storage.contract/verify runs against a cooperative reference
quorum that models at-most-one-per-height and nothing else. It checks that the
ADAPTER refuses a loser, reports the winning head, and never claims a publish
it cannot read back — it is not evidence about agreement. The evidence about
agreement is the test above, on the real commit rule.
Passing a conformance suite with an agreeable oracle is the easiest way to believe something false, so the claim is stated narrowly on purpose.
Likewise the "signatures" in the tests are witness|bytes strings, not a
curve. verify-fn is injected precisely so the curve lives at the edge
(Workers already carry @noble/curves), and what the tests need to exercise
is counting and distinctness, which a real curve would not make sharper.
While every witness is under one operator this is crash fault tolerance, not Byzantine fault tolerance, and superproject ADR-2607110300's rule says to call it that. A protocol being BFT and a deployment satisfying BFT's premises are different claims. Do not describe deployments using this library as "distributed" or "decentralized" until independent third-party operators with economic exposure exist (that ADR's Phase 4).
They do not run. The .cljc decides, and that is a measured choice.
kotoba/fuel.kotoba and kotoba/quorum.kotoba are the reference for the two
numbers a replica cannot be alone in believing. They are compiled, checked in
as .wasm, and compared against the live .cljc by the cljs suite. What they
are not is the thing production calls.
That is worth stating plainly because the workspace pattern is the opposite. Superproject ADR-2608112100 says a core with a parity test is not migrated — completion is the host executing the shipped artifact — and five cores in three repos have since moved that way. inga was measured against the same standard and did not move. The reasons are specific to consensus rather than to effort, so they are recorded here instead of left as an absence.
shipped .wasm |
KIR interpreter | |
|---|---|---|
| per call | native, but the instance traps | 1.35 ms on node, 1.0–1.6 ms on JVM |
| calls per instance | 256 (quorum-size), 42 (fuel/applied) |
unbounded; fuel resets per call |
| re-arming | 0.75 ms per fresh instantiation | n/a |
| both runtimes | needs a host per runtime (Chicory on JVM) | portable .cljc, verified on both |
| what a replica must share | the bytes | the interpreter pin |
Neither column is an argument about elegance. Each has one entry that disqualifies it here.
The wasm traps. A compiled module carries its own fuel and halts with
unreachable when it runs out — 256 calls of quorum-size on one instance,
measured. inga.fuel's whole rule is that exhaustion must be a value in the
state, never a throw, because a replica that throws has left the protocol
while its peers produce a root. Delegating the quorum threshold to something
that traps would put that exact failure on the path that decides whether a
certificate forms. Re-instantiating around it means catching a trap and
retrying, which is indistinguishable from catching a real error.
The interpreter makes the answer a function of a pin. This is the sharper
one, and it is not a dependency-count objection. quorum.kotoba's own header
states the stake: two implementations disagreeing means two replicas commit
different state. Delegating to a versioned interpreter does not remove the
second implementation — it replaces "inga's .cljc twin, which ships with inga
at inga's own pin" with "whatever kotoba-kir each replica happens to have",
and the substitute is worse in the way that matters: inga cannot see it, cannot
hold every replica to it, and cannot test both its runtimes without shipping
both. The hazard is not hypothetical. Measured on kotoba-lang/kura the same
week, that interpreter's ClojureScript path threw where its JVM path did not,
at pins the fleet was using — and ClojureScript is the runtime this library
deploys on.
A conflicting certificate at one height is the outcome the whole safety lemma
exists to exclude. It is the one place a version-dependent answer cannot be
tolerated, which is why the same delegation that is right for
kotoba-lang/crdt — where a disagreement degrades an ordering and the
primitives still converge — is wrong here.
Two costs of not delegating, stated rather than left for a reader to find:
- The
.cljctwin remains, and the parity suites remain load-bearing. The rules are written twice, andinga.fuel-kotoba-test/inga.quorum-kotoba-testare what keep the two honest. That is the arrangement ADR-2608112100 calls a first step, and here it is the last one. .cljcis not pin-free either —quoton a JVM long and on a JS number are different implementations. inga's answer to that isinga.parity: one scenario, both runtimes, one digest. Delegation would not have removed that problem, only moved it somewhere inga does not control.
The shipped binaries reproduce. Measured at the compiler pinned in deps.edn,
compiling kotoba/*.kotoba today produces kotoba/*.wasm byte for byte,
and two compilations of one source agree.
inga.kotoba-provenance-test reads the records the compiler writes and says of
itself that it does not claim reproducibility — a record proves the bytes are
the ones whose digest was written beside the source whose digest was written,
not that recompiling produces them. inga.kotoba-reproducibility-test closes
that, because the missing ingredient turned out to be one line: the builder
commit, which ADR-2608120200 named as the fleet-wide gap. It is the compiler
pin in deps.edn :test, and the gate recompiles with it.
Owning that pin rather than tracking the fleet's is what stops the gate being
theatre. The same ADR declined to write a recompile gate for
kotoba-lang/provider because it would go red every time the compiler moved;
when the repo owns the pin, red means the recorded builder is no longer the
one that made these bytes, which is a fact worth surfacing. Moving it is a
real migration and not a bump — measured on kura, the compiler on main
rejects sources these pins accept.
kbb -M:test:gen # rebuild kotoba/*.wasm and their provenance recordsADR-2608038000 names three things Filecoin's SPC / FVM / FEVM contribute that a chained-HotStuff ordering layer does not. All three are here; each composes with something that already existed rather than reinventing it.
A machine seam returning "113:c51298e1" lets replicas compare state and
nothing else — they cannot sync it, query it, serve it, or point a ref at it.
Filecoin's on-chain state is an IPLD structure, so a state root is something
you can walk; FEVM maps SLOAD/SSTORE onto that instead of a
Merkle-Patricia Trie, and pays exactly one method for it (eth_getProof).
inga.state takes the same shape and introduces no HAMT — the workspace
already has content-addressed maps. kotoba-lang/arrangement already
snapshots a 4-index datom db into prolly-trees, CID-addresses the commit
(dag-cbor of {schema-version index-roots prev}, every root a real tag-42
IPLD link), restores from it, and queries it with Datalog. None of that is
rewritten; inga.state wires it to the machine seam.
The added distinction is :root-kind. A machine declares :cid
(hydratable) or :opaque (a digest — still legal; a typed-array order book is
not obliged to become datoms to reach consensus). assert-hydratable! refuses
:opaque for anything backing a kotobase ref, which turns ADR-2608038000 D6's
"F1 must come before D6" from something a reader remembers into something the
code holds.
Acceptance, as the ADR stated it — four replicas reach the same CID and the state hydrated from that CID answers Datalog. Two levels, because the first alone was not what the ADR promised:
Standalone — four folds of the same op list produce one root, so the root is a function of the data:
(is (= 1 (count (set roots))))
(let [restored ((:hydrate-fn m) root uncrypt)] ; a reader with only the CID
(state/query restored {:find '[?s] :where '[[?s "role" "witness"]]}))
;; => #{["alice"] ["bob"]}Through the consensus (four-replicas-that-had-to-agree-reach-a-hydratable-root)
— four replicas that had to vote their way to a committed prefix, then
recomputed at a common height and hydrated. Folding four times proves the root
is deterministic; it does not prove replicas that had to agree on an order
arrive at one. The socket harnesses do go through a commit rule, and their
machine is an opaque digest — exactly what F1 replaced. So this half was
covered twice and joined nowhere until now.
An arbitrary apply-fn bounds no work and guarantees no agreement between two
implementations. engi hit the second half for real: a machine map holding a
ready-made order book handed every replica the same mutable structure, and
four replicas agreeing on 123 committed blocks differed by 200 resting orders.
Filecoin's FVM answers this by metering every operation, so determinism is a property of the VM rather than of each actor's care. Kotoba already has the primitive — the compiler's native backends implement fuel accounting.
inga.fuel's one rule: exhaustion is a value in the state, never a throw.
A replica that throws has left the protocol — it produces no state and no root
while its peers produce both. So apply-metered stops, records where, and
record folds that into the state the root commits to. Determinism depends on
exactly three inputs — budget, cost function, op order — and nothing else. Cost
is charged before the op, which is what makes the budget a real ceiling
rather than an approximate one.
The arithmetic now has a Kotoba implementation. kotoba/fuel.kotoba
compiles with kotoba compile --target wasm32 and the resulting
kotoba/fuel.wasm is checked in, the same way gftdcojp/engi checks in its
settlement module. inga.fuel-kotoba-test instantiates that binary and
compares it against inga.fuel across a 120-case matrix; the Kotoba module is
the reference, and if they ever disagree the Kotoba answer is the correct one.
This was previously written up here as blocked on the compiler's capability kits. That was wrong, and only checking found out: those blockers are about effects, and a state machine performs none — so no capability is declared, the deny-by-default policy has nothing to grant, and the module compiles as-is.
What checking also found: the compiler's fuel is not inga's fuel. A
compiled module carries its own budget per instance and traps with
unreachable when it runs out — measured at 42 calls of applied(10,1,100)
on one instance. That is the compiler doing its job, and it is exactly the
failure inga.fuel forbids:
| exhaustion is | who can check it | |
|---|---|---|
inga.fuel |
a value folded into the state root | any peer |
| compiler fuel | a trap | nobody, after the fact |
So any deployment running the machine as a Kotoba module must give every
replica the same initial fuel (--fuel / --fuel-initial), or replicas
trap at different call counts and diverge for a reason that has nothing to do
with the transactions. A test pins the trap behaviour so this note cannot go
stale silently.
Still not a full .kotoba machine body: the fold is metered in Kotoba, the
op application is still cljc. And the module is the reference, not the
execution path — inga.fuel decides. That is measured rather than pending;
see What the Kotoba cores actually run,
where the trap above is one of the two reasons.
engi.stake already implements permissionless admission by external
collateral, stake-weighted quorum, equivocation-only slashing, and a
role-tagged single bond market. None of that economics is reimplemented
here. The one thing it cannot do by itself is the thing F3 is about: it is
handed the bond map from outside, so who is a witness is decided somewhere
the consensus does not order — and a validator set is not a thing peers may
disagree about, because it is what quorum is counted against.
inga.power is the table plus the transition function a machine applies, so
the table at height h is a function of the committed prefix and nothing
else. :storage joins :ordering and :recompute on the existing market
— SPC's idea that the Sybil-resistant resource should be the useful work the
network does, which here is retaining and serving datom blocks.
It does not replace external collateral (engi's reason for bonding USDC rather than EN is unchanged: EN nets to zero, so bonding it disincentivises nothing).
inga.retrieval credits the role. Filecoin needs PoRep and PoSt because a
storage proof there must survive an adversary who can fetch the data from
anywhere and is paid to look like a storer. A datom plane needs far less for a
much weaker claim, because the data is content-addressed: "return the
bytes for this CID" verifies itself — hash what came back and compare. No
setup, no sector, no proving time; the whole verifier is one hash and one
comparison.
A passing sample proves at sample time, this witness could produce these bytes — and three things it does not:
- durable storage. A witness that fetched the block from a peer the moment it was asked passes. Nothing distinguishes holding from fetching.
- unique storage. Ten witnesses can pass on one physical copy.
- future availability. The sample is about the instant it ran.
The economic argument against (1) is that fetching on demand costs more than storing when samples are frequent and unpredictable enough — an argument, not a measurement. No deployment using this may claim Filecoin-equivalent storage guarantees.
The challenge is derived from a value fixed after the storage claim and by
nobody in particular (a committed block hash), because a witness that can
predict its challenge stores only what will be asked for. Deriving it from the
clock or from a caller's choice would be the same hole from the other side.
A witness claiming nothing is :unproven, not :pass — otherwise the
cheapest way to look like a storage provider is to claim to store nothing.
inga.{consensus,quorum,pacemaker,replica,sync,wire,net,attest,stake} came
from kotoba-lang/engi, where chained HotStuff had grown up inside a currency
repo. The reason to move it is in engi's own consensus.cljc docstring:
engi does not know what a transaction is, and must not — a consensus layer that imports either application becomes "a consensus layer for exactly one application".
| ns | what it owns |
|---|---|
inga.consensus |
block / QC shape, n=3f+1 and quorum=2f+1, the chained 3-chain commit rule, round-robin leader rotation |
inga.quorum |
quorum as one predicate, count-based and stake-weighted |
inga.pacemaker |
views, timeouts, timeout certificates, view change |
inga.replica |
the replica itself — adopt, commit, the machine seam, equivocation recording, bounded snapshot/resume |
inga.sync |
catch-up over segments a stranger hands you |
inga.wire |
total decode; JSON has no keywords, so :inga.block/height travels as "height" |
inga.net (+ net/server, net/ws) |
the WebSocket transport, both halves |
inga.attest (+ attest/ed25519) |
signatures on certificates — what makes a quorum a quorum rather than a list of names. Ed25519 through WebCrypto, so a Worker needs no dependency to verify one |
inga.stake |
permissionless admission by external collateral, stake-weighted quorum, equivocation-only slashing |
inga.attest is not a duplicate of
kotoba-lang/witness-quorum,
though a docstring here claimed a dependency on it for months. They solve
different problems: witness-quorum cosigns an already-written CID after the
fact (a Certificate-Transparency shape, with a 3-layer validation membrane);
inga.attest signs votes and certificates before a commit, inside the
protocol. They overlap only at "Ed25519", and differ there on purpose —
witness-quorum's cljs signer takes @noble/curves from npm, inga.attest
uses WebCrypto and takes nothing, because this has to run in a Worker.
Because the namespaces moved, :engi.block/* keywords became
:inga.block/*. That is not a wire change — inga.wire's own docstring
notes JSON has no keywords, so only the local name changed.
inga.power (F3) was written before inga.stake arrived and had simplified
copies of eligible / stake-for / quorum-met?. They are gone: inga.stake
owns those, and two implementations of a quorum rule is not redundancy, it is
two answers to "did this block commit". What power keeps is the part stake
genuinely cannot do — making the bonds map a function of the committed prefix.
kbb --backend sci --classpath "src:<torihiki>/src:<bytes>/src" script/torihiki-on-inga.cljkFour replicas over real WebSockets, each executing torihiki.state/apply-block
on the blocks inga commits, then asked whether they hold the same exchange.
Run before the extraction (from engi) and after (from inga); both pass:
common committed blocks: 43
every replica the same : true
the thief's order : refused {:not-your-account 2, :wrong-key 23}
TORIHIKI-ON-INGA: pass — four replicas, one exchange
replay reconstructs a replica by re-executing every block it ever adopted.
That is the right thing for VERIFYING a chain and the wrong thing for STARTING
one: it costs O(chain), and a process that must pay that before it can answer
anything eventually cannot start at all.
Measured, not hypothetical. On 2026-08-04 a deployed validator running this consensus layer spent long enough replaying its log to exceed a Cloudflare Durable Object's CPU budget; the platform reset the object, the reset threw away the in-memory state, and the next invocation replayed from zero and exceeded it again. A crash loop that could not end, because recovery was the work that killed it.
snapshot keeps the last resume-tail blocks (8 — the 3-chain rule needs
three to derive a commit and ancestor? walks parent links, so four is the
floor), the certificates naming them, the pacemaker, the machine state, and
nothing that grows with the chain.
A replica must never sign different blocks in one view. The durable
:voted-view stops a second signature in that view even if sync changes the
tip or restart clears the vote cache. A cached vote can be resent byte for
byte; a later view may sign again under the pacemaker's safe-to-vote rule.
replay was reconstructing :voted by folding the blocks. A bounded
snapshot cannot, by definition.
So the set is replaced by a height watermark: :voted-below = the tip
height, and voted? answers true at or under it whether or not the set still
names the height. This reconstruction is conservative, but it cannot replace
the view watermark: an uncertified vote may not be present in the adopted
chain. A host must persist the new snapshot before sending its vote.
test/inga/resume_test.cljk was written before the implementation and asserts
the property directly — every block the replica holds is offered back to it and
every vote that leaves is checked against the block it actually adopted.
script/test-vote-watermark.cljk checks the final signing gate with real
Ed25519 signatures: alternate-tip sync and restart cannot produce conflicting
same-view votes; a cached vote is resent unchanged; a later view can vote;
a conflicting locked tip is refused.
This guard does not repair a previously forked chain or fence a consumer that
has already begun a side effect. A production writer needs a fresh qualified
chain and an operation-level fencing check.
commits had to stop counting. It compared (count (:committed state))
against three-chain-commits over the whole chain, which assumes both run
unbroken from genesis. Over a tail, the count exceeds the list, drop returns
nothing, and the replica commits nothing ever again while every number about it
looks healthy. It compares by height now — the correct formulation with or
without a tail.
:first-vote is dropped, and with it the equivocation evidence this
replica had collected about others below the tail. Evidence already broadcast
is held by peers; evidence not yet broadcast is gone. Stated rather than
hidden.
engi does not depend on inga. Its remaining namespaces required nothing from the consensus set, which is why the cut was clean — but it also means inga's consumer today is the torihiki harness, not the ENGI ledger. Running ENGI/EN on inga is future work.
An equivocation used to be recorded by whichever replica happened to receive both conflicting votes, and go no further. That punishes nobody: the equivocator only has to keep any single peer from seeing both, which is a routing property it can influence.
:evidence is now a wire message. A replica that detects an equivocation
broadcasts the proof; a replica that receives one verifies it before
recording (inga.stake/verify-equivocation-evidence re-checks the whole
claim — same witness, same height, different blocks, both signatures — because
otherwise evidence is a way to accuse anyone of anything), records once per
[witness height], and forwards only on first sight, so one proof does not
become a permanent storm between peers.
script/network.cljk grew a BYZANTINE_SPLIT=1 mode that sends the
equivocator's second vote to one peer instead of all — the case the
original harness never created, because broadcasting both votes to everyone
makes every replica an independent detector and never asks whether a proof can
travel. Measured over real sockets:
BYZANTINE_SPLIT=1 |
w1 | w2 | w3 | |
|---|---|---|---|---|
| without propagation | 14 | 0 | 0 | NETWORK: FAIL |
| with propagation | 36 | 8 | 23 | NETWORK: pass |
Without it, an equivocator that routes its two votes to different peers escapes entirely.
script/network.cljk used to report NETWORK: FAIL — an honest replica holds no proof against the equivocator on about one run in four. I first wrote that
up as a timeout being too short. That diagnosis was wrong. Counting what
the byzantine validator actually cast:
| equivocating votes cast | honest replicas holding a proof | |
|---|---|---|
| passing runs (12 measured) | 75–147 | all 3 |
| the failing run | 12 | 0, 0, 0 |
All three at zero rather than some at zero is the shape of it barely voted, not the proof did not travel. The validator sat out; the protocol did nothing wrong.
So the assertion is now judged only when there was something to judge — below
min-twins-to-judge the run reports INCONCLUSIVE, loudly, with the count,
and everything else still has to pass. deliver-all's own docstring says why
this matters: an intermittent test is worse than none, because it teaches you
to re-run it.
replica used to take :quorum and, when omitted, silently fall back to
counting its own witness list. Head-counting is correct for a managed set
and is exactly what a Sybil defeats under permissionless admission — and
nothing in the built state said which one you had.
inga.quorum/profiles is now a closed set, modelled on
kotobase.storage.core/ref-profiles and for the reason that namespace gives:
the failure mode of guessing is silent.
| profile | what it resists |
|---|---|
:head-count |
nothing, against an adversary who can register witnesses. Correct when who may hold a key is decided outside the protocol. |
:stake-weighted |
Sybil identities: splitting a bond across more of them changes the head count and not the stake. Requires a bond source. |
The convenient default is unchanged — every managed deployment wants it — but
a replica now carries :quorum-profile, so what a deployment actually resists
is a value you can ask it for. An unlabelled predicate reports :head-count
rather than being upgraded on the caller's behalf.
Same arrangement F2 uses for the metering arithmetic, applied to the other
number a replica cannot be alone in believing. kotoba/quorum.kotoba compiles
to the checked-in kotoba/quorum.wasm, and inga.quorum-kotoba-test
instantiates that binary and compares it against the live inga.consensus /
inga.quorum / inga.stake functions. The Kotoba module is the reference; if
they ever disagree, the Kotoba answer is the correct one.
What crossed is arithmetic and nothing else — quorum-size,
byzantine-tolerance, one-honest, the comparison inside at-least's
closure, and the >2/3-of-stake rule with its unbonded head-count fallback and
the branch that chooses between them. Counting distinct witnesses, summing
bonds, verifying signatures, attaching a ::profile to a closure and
->predicate's dispatch on a runtime type all stay in the .cljc: they are
folds and dispatches, not decisions. stake-quorum-met? reduces to a
two-integer comparison the moment its two sums exist, and the test makes that
seam visible — inga.stake/total-stake produces the sums, the guest decides.
A disagreement about a threshold is worse in kind than one about a budget. A
replica computing a smaller quorum than its peers forms certificates the rest
of the network will not accept; one computing a larger one refuses valid ones.
At n = 6 the safe rule says 4 and the familiar 2f+1 shortcut says 3 — and two
disjoint 3-subsets of 6 are two conflicting certificates at one height, which
is the outcome inga.consensus claims cannot happen. The matrix therefore
walks well off the n = 3f+1 grid, where that shortcut stops being safe.
Every function in the module is i64 -> i64, using only + - * quot and
comparison, which is the word-typed slice
kotoba.kir/only-native-word-typed-features? admits — so it also compiles for
the native AOT backends, not only wasm32. That claim is now checked rather than
asserted (inga.kotoba-reproducibility-test); it used to be prose.
The module is the reference and not the execution path — inga.consensus /
inga.quorum / inga.stake decide. Why the port stops there, with the
numbers, is What the Kotoba cores actually
run.
inga.stake implements bonding, stake-weighted quorum and equivocation-only
slashing; inga.power makes the bond table a function of the committed
prefix; inga.quorum/stake-weighted plugs into replica. The seam is
complete and unused, because there is no bond source.
kotoba-lang/engi-witness-escrow
is the on-chain custody half — reviewed, tested, and deliberately not
deployed. Its own README says why: deploying a contract that custodies real
third-party money is a separate, higher-stakes decision requiring its own
explicit review. That decision is the blocker, and it is not an engineering
one. Until it is made, deployments run :head-count and have no economic
security — which is now something the state says out loud.
Still open, from the same ADR: the .kotoba machine body is blocked on the
compiler, and bond collateral is not deployed, so slashing is implemented
and does not fire.
Two items that used to sit in this list are done and were left stale here:
:storage power has retrieval sampling (inga.retrieval, above), and
equivocation evidence does propagate — replica broadcasts a proof on
detection, verifies before recording, and forwards only on first sight, with
the BYZANTINE_SPLIT=1 measurements above showing what it buys.
Added 2026-08-05 (superproject ADR-2608055000 G2). inga.power's :slash
used to validate only that the witness had a bond, resting on a comment
saying evidence was checked before the event arrived. Nothing enforced
that, and the evidence never travelled in the committed event — so no replica
could have checked it even in principle. Whoever composed the block decided
who lost their collateral, and every replica applied it deterministically.
Ordered, reproducible, and still an adjudicator.
Now :slash carries :evidence, and apply-events takes a
:verify-sig-fn that inga.stake/verify-equivocation-evidence re-runs over
it. Four things are refused rather than applied: no evidence, evidence naming
a different witness, evidence that does not verify, and evidence already
punished (proofs do not expire, so without the last one a witness that was
slashed, re-bonded, and behaved since could be confiscated again forever).
Refusals are RECORDED in :rejected-slashes, not thrown. A slash is the one
event anyone may submit about anyone, so throwing would let a single forged
accusation halt every replica — and dropping it silently would make a flood
of them invisible. Whether evidence verifies is a function of the committed
bytes, so refusing is deterministic and does not diverge; that is what
separates this from the :default method, where "unknown" depends on the
replica's code version and therefore must throw.
They are counted, not listed: {witness {reason count}}. The first
version appended a map to a vector, which was wrong for the same reason the
throw was — that is attacker-chosen data in state folded across the whole
chain and hashed into a state root, and a proposer could grow it without
bound forever at the cost of block space alone. The tally is bounded by the
witness set times the fixed reason set and answers the same question. By
contrast :slashes keeps its vector, because only a successful slash
appends to it and each one costs the offender their entire bond: the
attacker does not choose that one.
Missing :verify-sig-fn does throw. A replica that cannot check evidence
must not quietly record refusals while its correctly-configured peers apply
the same slash for real. Every replica passes the same verifier, exactly as
they must share :hash-fn.
See the workspace license policy.
inga.reservation/verify-acquisition verifies the first finalized seq-zero
head for a permanent slot. It composes the existing signed segment validator,
three-chain commit rule, and inga.ref/project; it does not alter
inga.commitment/verify-head (membership).
(reservation/verify-acquisition
record
{:blocks [genesis block-1 block-2 block-3] :tip-qc tip-certificate}
{:genesis genesis :chain-id "my-chain"
:witnesses ["w1" "w2" "w3" "w4"] :quorum 3 :max-blocks 256
:hash-fn hash-block :verify-fn verify-signature :decode-proposal decode})
;; => {:status :reserved :record winner :block-hash hash}
;; or {:status :unresolved/:rejected :reason keyword}Genesis must be trusted and empty. The proof contains every block from that genesis; the tip is certified and the selected head must be in the three-chain finalized prefix. Truncated history, absent progress and a lone membership certificate cannot grant a reservation. The proof is deliberately O(history), capped by caller policy; hitting that cap is a refusal, not a reason to omit history. A committed-state checkpoint/proof is future work.
This verifies an acquisition, not a send capability. The CID must bind a
consumer's authenticated holder, request identity and exact intent. There is
no expiration, release, or reassignment: seq greater than zero cannot acquire
this slot. The recipient/signer still needs durable consumption, deduplication
and fencing before doing external IO; two workers presenting the same winning
CID both have the same evidence. Do not deploy this as a payment journal or
restore a legacy HeadStore. The node's existing {block, qc} sidecar does not
serve the full proof this API needs.
The guarantee is conditional on a static correctly configured validator set, its quorum fault bound, correct locking/voting, durable state before votes leave, no key clones/rollback, and a canonical collision-resistant block hash. An equivocating quorum can sign two incompatible histories; the qualification test explicitly demonstrates that limit instead of interpreting signatures as proof of operational honesty. Dynamic validator changes are not qualified.
script/test-reservation.cljk runs the qualification tests on nbb with real
Ed25519 keys, real replica transitions and an in-memory partitionable
transport. Supply src, test, inga-node's src (test crypto seams), text's
src, and kotobase-storage's src on the classpath. This extends the existing
CLJC consensus component; it is not a Kotoba guest migration or production
qualification. Holder authentication, receiver fencing, OS power-loss tests
and live multi-host partitions remain separate work.
inga.prefix/verify exposes the same complete-prefix verification independently
of reservation records, so archive ingestion/readback and reservation consumers
cannot drift on finality. A bound overflow returns :max-blocks-exceeded;
malformed or membership-only input returns a rejection. verify-acquisition
uses this shared path. This remains a complete-history proof, not a state-root
checkpoint or a proof of the latest available chain tip.
After the upstream .cljk rename, nbb 1.4.208 does not resolve transitive
.cljk namespaces. The canonical source is kept as .cljk; historical-suffix
mirrors are diagnostic only, not a supported entrypoint or deployment artifact.
The shared toolchain must register .cljk namespace resolution (including
platform-collision names recorded in cljk-origin.edn) before this nbb suite
can qualify the canonical source layout.
inga.paged-prefix verifies the inga-node opt-in archive pages without a
total block-count ceiling. Call begin with a trusted empty genesis, ordered
witness keys, chain ID, quorum, real hash and signature functions, and the
existing context fields. Feed each decoded
/local-finalized-range?from=<height>&limit=128 response to append-page
in order, then call finish with a fixed /local-finalized-checkpoint
response. Only finish returning {:status :finalized ...} authenticates the
complete prefix. The client must cap each HTTP response at 8 MiB before
parsing EDN and must not restore verifier state from an untrusted peer.
Each page is limited to 128 blocks. The verifier checks exact genesis,
contiguous heights, parent links, proposer order, parent QCs, checkpoint
anchor, both finality descendants, and the signed tip QC. It retains the last
block and a two-block finality window, so verification space is bounded by
one page plus those blocks. Work and network transfer remain O(history), and
this is not a succinct state root or proof that no newer tip exists. Tests
compare it with inga.prefix/verify using real Ed25519 certificates, reject
gaps and tampering, and cross the 8192-block boundary with a cheap synthetic
signature seam. The latter checks scaling and boundaries, not cryptography.
inga.segmented-prefix/verify-acquisition is an additive, pure candidate API.
Existing inga.prefix/verify and inga.reservation/verify-acquisition retain
both their formats and bounds. The new API checks the same first finalized
seq-zero winner while decoding one segment at a time. It never concatenates a
second full block vector or builds a projection for every ref. It still reads
and verifies all history from genesis on every call: O(history). It is not
a trusted state-root, a succinct proof, a latest-tip oracle, or production
reservation qualification.
The invocation is (verify-acquisition record checkpoint segments context).
segments is an ordered, materialized vector of {:cid raw-cid :data edn-text}.
Each CID must be the canonical base32 raw CIDv1/sha2-256 address of the exact
UTF-8 text. The trusted context adds two pure functions to the existing trust
and proposal-decoding seams: :segment-cid-fn computes that real CID, and
:text-byte-count computes UTF-8 byte length. A fake hash/decoder/signature
callback defeats the guarantee. The host must preserve exact text and use
fatal UTF-8 decoding when starting with bytes.
The checkpoint has exactly these keys:
{:version "inga.segmented-prefix/v1-candidate"
:chain-id chain :genesis-hash genesis-hash
:last-cid final-segment-cid :segment-count count :block-count total-blocks}Each CID-addressed envelope has exactly these keys:
{:version "inga.segmented-prefix/v1-candidate"
:chain-id chain :genesis-hash genesis-hash
:index zero-based-index :prev-cid previous-segment-cid-or-nil
:from-height first-height :to-height last-height
:blocks [first-block ... last-block] :tip-qc signed-certificate-for-last-block}Segment zero starts at the exact trusted empty genesis and has nil prev-cid.
Later segments start at the preceding height plus one, without overlaps. Every
CID and predecessor link is checked; each segment's chain, genesis, index and
height metadata must match its contents and the checkpoint. inga.sync checks
parent links, contiguous heights, leader/round rules and parent QCs across
segment boundaries as well as within them. Each segment tip needs its own
valid QC. A genesis-only first segment uses the existing trusted-genesis
bootstrap exception: its QC must name that exact genesis and height zero,
but need not have quorum signatures. It cannot finalize a reservation. The rolling two-block tail preserves the existing three-chain commit
rule. No winner or partial success escapes until the final segment, CID and
count checks complete. All finalized same-ref records are checked even after a
winner is found. Missing, duplicate, reordered, mismatched-branch and suffix-only
histories fail closed. The wire profile permits plain/namespaced EDN maps,
vectors, sets and scalars, without tags, discard forms or character literals;
a depth scan bounds recursive parsing before the EDN reader.
Finite ceilings are constants; optional :limits may only lower them:
| Resource | Ceiling |
|---|---|
| Materialized input UTF-8 text | 32 MiB |
| Segments / blocks per segment / total blocks | 64 / 128 / 8,192 |
| One segment's UTF-8 text | 1 MiB |
| Proposal bytes / proposals per block / total proposals | 8 KiB / 64 / 32,768 |
| Witnesses / conservative QC signer-check budget | 32 / 65,536 |
| EDN nesting depth | 64 |
| Successful result's serialized UTF-8 size | 16 KiB |
Refusals are constant-sized status/reason maps. At most one segment is decoded
at a time, with two pending blocks and one winning record retained; the pending
vector is copied so a subvector cannot accidentally retain the full history.
A block may itself approach the segment-size bound. Parsed object, UTF-16 and
allocator overhead mean these are not byte-exact heap/RSS limits. Caller input
allocation happens before this API; the future download adapter must impose
bounds before buffering. Total byte scanning, canonical hashing and proposal
work are linear under the ceilings; block-hash calls are conservatively bounded
by 4 * max-total-blocks + 4 * max-segments + 4, and segment-CID calls by
max-segments. These are logical-work bounds, not wall-clock CPU guarantees
for arbitrary injected functions. Overflow is a refusal, not permission to
skip segments or increase an untrusted budget.
The synthetic suite compares first-wins, loser, unresolved-tail and malformed
same-ref outcomes with the existing verifier over multiple segment widths,
including boundaries of one block. It uses real Ed25519 signatures and raw
CIDs, re-addressed semantic tampering, branch splices, missing/reordered/suffix
inputs and resource limits. script/test-reservation.cljk now also needs the
archive test codec's dependencies (inga-node/src, kotobase-client/src,
sha2/src, text/src, kotobase-storage/src) alongside src and test.
The candidate itself adds no runtime dependencies.
The current inga-node v1 archive publishes one full-prefix bundle. Neither its producer nor Draft PR #4's bounded startup lifecycle emits this segment format. A future version must retain genuine history before the eight-block snapshot window loses it; produce sequential CID-linked envelopes and actual tip QCs; verify every object after PUT/readback; then atomically persist the complete checkpoint locator. Only segments wholly at or below the verified finalized height may be sealed: a segment-tip QC alone does not finalize its last two blocks. A changed segment containing an unfinalized tail has a new CID, and any descendant links must follow that new CID. Never rewrite finalized content or invent a missing segment after restart. Recovery must walk the entire predecessor chain within download/count/byte budgets and verify genesis, all boundaries, QCs and counts before returning a reservation result. No storage-side assertion or state-root may bypass that walk. This requires separately authorized fresh Kotobase signing, download cancellation/backpressure and restart/gap tests; none is enabled by this pure candidate.
The same external assumptions remain: the configured static validator set and ordering are authentic, quorum fault limits and voting/locking rules hold, votes are durably recorded before send, witness keys are not cloned or rolled back, hashes are collision resistant, and proposal decoding is deterministic. A quorum that equivocates can certify two complete alternate branches; segment CIDs detect substitution within a chosen chain but cannot make that quorum honest. Validator rotation, holder/intent authentication, receiver fencing and exactly-once external consumption remain outside this proof.
Owns ordered state/head and consensus contracts for declared domains. Keeps deterministic transition/metering separate from transport, placement and external Ethereum/FEVM settlement. Shared assets, policy governance and shared heads declare required finality; validation or DHT replication alone cannot satisfy it. Actual network/quorum/crypto evidence is separate from pure contract checks.
See the owner integration guide and dependency direction, composition metadata, and whole-stack refactor procedure. The direction is adopted; runtime contract migration and qualification remain explicit, separately verified work. Tier labels are responsibility axes, not a single dependency ranking.
Execution descriptors are owned by kotoba.core.execution in core-contracts;
Component/WASI admission belongs to kotoba.abi.component in abi. Upstream
consumer pins follow these entrypoints. Native remains an independent AMU target,
and legacy v1 wire fields/CIDs remain compatible. See
the integration spec and
whole-stack procedure.
The dependency update does not enable default v2 runtime admission or qualify
new target/host/consistency combinations.