Skip to content
kotoba-langPublic

About

因果 — the consensus plane of the kotoba stack. A ref plane whose compare-and-set is a 2f+1 quorum certificate, not a host primitive.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

inga — 因果

The consensus plane of the kotoba stack. Ordering, state, execution.

Implements ADR-2608038000 (com-junkawasaki/root).

⚠ Three repos in this org use Buddhist causality words for different things

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.

inga is one letter from engi and one concept from innen, and in west.yml inga and innen land on adjacent lines. The collision is known and accepted (superproject ADR-2608038000 D5, same treatment as kotobase / kotobase-client / kotoba-client in ADR-2607050900) — this table is the mitigation, so it is required to stay at the top of all three READMEs.

Why the name

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.

What is here today

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.

Two proofs, and why there had to be a second one

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.

The one idea

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+1

The 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.

Verification

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 assertions

Both 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.state at all — SCI dies with Protocol not found: IEquiv inside a transitive dependency. shadow-cljs is the compiler kotobase actually deploys with, so that is the vehicle; nbb still runs inga.parity.
  • arrangement's platform split is not only in what it returns. On cljs it also expects blind-fn / encrypt-fn / decrypt-fn to 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.

The oracle is gone: the commit rule is the CAS

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.

What that still does NOT establish

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.

Labelling

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).

What the Kotoba cores actually run

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.

The two seams, measured 2026-08-12

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 .cljc twin remains, and the parity suites remain load-bearing. The rules are written twice, and inga.fuel-kotoba-test / inga.quorum-kotoba-test are what keep the two honest. That is the arrangement ADR-2608112100 calls a first step, and here it is the last one.
  • .cljc is not pin-free either — quot on a JVM long and on a JS number are different implementations. inga's answer to that is inga.parity: one scenario, both runtimes, one digest. Delegation would not have removed that problem, only moved it somewhere inga does not control.

What did change: the source is now the authority for the binary

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 records

The Filecoin half — F1 / F2 / F3

ADR-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.

F1 — the state root is a CID, not a digest

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.

F2 — running out of fuel is a state transition, not an exception

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.

F3 — the power table is committed state

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:

  1. durable storage. A witness that fetched the block from a peer the moment it was asked passes. Nothing distinguishes holding from fetching.
  2. unique storage. Ten witnesses can pass on one physical copy.
  3. 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.

The consensus — extracted from engi 2026-08-03

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.

Acceptance

kbb --backend sci --classpath "src:<torihiki>/src:<bytes>/src" script/torihiki-on-inga.cljk

Four 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

Restarting without folding the whole log — snapshot / resume

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.

The dangerous part is :voted

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.

Two things this cost

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.

Not here yet

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.

Evidence propagates

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.

The catch assertion is conditional, and says so

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.

What a quorum resists is declared, not inferred

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.

The thresholds have a Kotoba implementation

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.

Slashing still does not fire, and that is a decision, not a bug

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.

A slash carries its proof, and the fold checks it

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.

License

See the workspace license policy.

One-shot reservation evidence (qualification API)

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.

Paged complete-history verifier

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.

Segmented complete-prefix candidate

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.

Producer/archive integration still required

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.

Target-neutral and distributed stack architecture

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.

Neutral execution and target profiles

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.

About

因果 — the consensus plane of the kotoba stack. A ref plane whose compare-and-set is a 2f+1 quorum certificate, not a host primitive.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors