AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
Pay for verified outputs, never for claimed effort. An auditable log, pinned checkers, and citation-flow attribution.
Developer toolsRust · SwiftGitHub @aburan28OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Loading the full README. The saved excerpt is shown below.
A research network where verified results are the unit of account.
Anyone contributes compute toward shared objectives, AI agents do the work, and
payment is settled by a checker that anyone can re-run. No trust in the operator,
no trust in the contributor, no trust in the model that produced the answer.
This repository contains cairn, the protocol implementation: a Rust
library and CLI, a second and deliberately independent Rust implementation in
reference/, and the conformance vectors that bind them to the same answers.Read the full README ↗A research network where verified results are the unit of account.
Anyone contributes compute toward shared objectives, AI agents do the work, and payment is settled by a checker that anyone can re-run. No trust in the operator, no trust in the contributor, no trust in the model that produced the answer.
This repository contains cairn, the protocol implementation: a Rust
library and CLI, a second and deliberately independent Rust implementation in
reference/, and the conformance vectors that bind them to the same answers.
Site: https://aburan28.github.io/cairn/ — what it is, how
it works, and the challenges a node is paying for. It is the same app a node
serves at /ui/ (see ui/README.md), so nothing on it is
simulated: the numbers come from a node when one answers and from
launch/cairn.jsonl when none does, and the page says which. On a phone,
Add to Home Screen, or the native reader in gui/ios. Neither
runs a node. The macOS app in gui/macos is the other job: it
runs the researcher.
Stage 0 — one operator, no token, no consensus. What it does provide is the property that actually matters: anyone can independently re-derive every result the network has settled, from nothing but a copy of the log.
$ ./scripts/interop.sh
== the reference implementation audits the primary log (--rerun on trusted fixtures)
log verified: chain intact, every settled claim re-verified
== the primary implementation audits the reference log
log verified: chain intact, every settled claim re-verified
== Merkle roots agree across implementations
sha256:f0398c1ae67875b4ecc3c9c0d674f7b44b6876b77445a40e1d8ce7f6b331a168 (identical in both)
INTEROP OK: each implementation verifies the other.
That is the claim made concrete. "Anyone can re-derive every result" is worth nothing if it means "anyone running my code"; two implementations written separately, sharing no code and not even a cargo workspace, agreeing on every id and every Merkle root, is what makes it real.
The second implementation earns its place by disagreeing. Building it caught
two bugs the primary's own test suite could not see: work assignment reading
four bytes of the HMAC where the format takes eight (two nodes would silently
overlap regions), and the genesis prev written as "" rather than null
(every entry hash shifts, while the stored Merkle root still matches). Neither
would ever have raised an error.
Pay for verified outputs. Never pay for claimed effort.
Almost every hard problem in decentralized compute — did the node really run the job, did it use the right model, did it burn the FLOPs it billed — exists only because the network is trying to buy work. Buy artifacts instead and most of it dissolves. Nobody can fake a Lean proof the kernel rejects, a counterexample that fails recomputation, or a program that scores badly on a fixed evaluator. The check is the payment condition, so a contributor's hardware, honesty, and diligence stop being things anyone has to verify.
The corollary is the whole engineering constraint: the network can only work on tasks whose outputs are cheap to check. That is a specification for what to build, not a limitation to route around.
curl -fsSL https://github.com/aburan28/cairn/releases/latest/download/install.sh | sh
Detects the platform, downloads the matching tarball, checks it against the
published .sha256, and installs the one binary a release contains — cairn,
whose subcommands (run, mcp, p2p, serve, gen-bootstrap, seeds,
arena)
replace what used to ship as separate executables — to ~/.local/bin.
--version pins a release, --bin-dir picks somewhere else, and on Linux
--libc gnu takes the dynamically-linked build instead of the static musl one.
Linux amd64/arm64 and macOS Intel/Apple Silicon are supported by this script.
For Windows, choose the native x64 portable ZIP or the PowerShell installer, which
installs the Linux release inside WSL 2 (see docs/install.md).
The ZIP contains cairn.exe, the embedded reader and a double-click launcher; it
needs no Node.js, Rust toolchain or administrator access. Native Windows external
verifier programs require a qualified sandbox and return Unavailable when one
is unavailable; WSL 2 provides the Linux sandbox path.
Or a native package, from the releases page:
| you have | download | then |
|---|---|---|
| macOS, either chip | cairn-<version>-macos-universal.dmg | open it, run Install Cairn.pkg |
| Windows 10/11, x64 | cairn-<version>-x86_64-pc-windows-msvc.zip | extract it, open Start Cairn.cmd |
| Debian, Ubuntu | cairn_<version>-1_amd64.deb (or _arm64) | sudo apt install ./cairn_*.deb |
| Fedora, RHEL 9 family | cairn-<version>-1.x86_64.rpm (or .aarch64) | sudo dnf install ./cairn-*.rpm |
The native Linux/macOS packages put that binary on PATH for every user; the
Windows ZIP stays in the folder you choose. Neither adds a service or account.
The macOS installer is unsigned, so macOS asks you
to approve it the first time. docs/install.md has that, what
each package recommends and why, upgrading, removing, and what to do when two
routes have both been used.
What that checksum does and does not prove. It comes from the same server as the tarball, so it detects a corrupted download and nothing else — there is no signing key here yet, and saying otherwise would be worse than saying this. The check that means something is the one this project exists for, and it runs against bytes that ship in the repository:
cairn --log launch/cairn.jsonl --root . audit
From source instead:
make ui-build # exports the reader, then builds with `--features ui`
cargo install --path . --features ui # puts the single `cairn` binary on PATH
The ui feature is off by default, and cairn run refuses to start
without it — the reader it serves at /ui/ has to be embedded at build time.
A plain cargo build --release needs no Node toolchain and produces every
other subcommand, but not a working cairn run; make ui-build runs npm ci && npm run build in ui/ first and then builds with the feature on. cargo build --release --features ui does the same if ui/out already exists.
docs/quickstart.md is this section at more length and with the output shown: audit the published log, win a bounty end to end, then start a node. What follows is the short version.
An installed release has the complete local node behind one command:
cairn run
This starts the MCP server, P2P synchronization, the HTTP API, a local
submission queue, and the embedded reader in one process. Open
http://127.0.0.1:8080/ui/. Its local state lives under .local/; P2P listens
on 127.0.0.1:9000, so the default is safe for a first run and does not expose
a node to the network. MCP uses stdin/stdout, so an agent client can use cairn
as its command with run as its only argument. Operational messages stay on
stderr and stdout contains JSON-RPC only. Closing MCP stdin stops the combined
node; an interactive operator can stop it with Ctrl-C.
A machine that should work for the network rather than hold its log -- a
GPU box, a cluster node -- runs the host agent instead: sudo cairn agent install --node http://node:8080 registers its CPUs, memory and GPUs with a
node every minute and runs executor jobs under gVisor or Kata Containers, as a
systemd service. docs/agent.md.
For example, a project-local Claude Code configuration can launch the complete
node instead of the standalone cairn mcp server (once a separate cairn-mcp
binary):
{
"mcpServers": {
"cairn": { "command": "cairn", "args": ["run"] }
}
}
Use --mcp-identity agent.identity.json after run to sign agent submissions.
Use --no-mcp when stdin belongs to something else, such as a service manager.
To join through a known bootstrap file, pass it after run:
cairn run --bootstrap .local/seed.json
To expose the listeners deliberately, choose the addresses explicitly:
cairn run \
--listen 0.0.0.0:9000 \
--serve 0.0.0.0:8080 \
--bootstrap .local/seed.json
Two machines in two homes, each behind its own router, is
docs/two-nodes.md: the daemon asks the router to forward
the p2p port (CAIRN_PORTMAP), and GET /sessions says whether anyone outside
has actually reached it. One leader that is paid, with workers and GPU boxes
on your own network that nobody else can reach, is
docs/fleet.md: the leader signs the fleet's records
(CAIRN_FLEET), and CAIRN_PEERS keeps a fleet of nodes to itself. What the
network is trying to beat, and from which angles, is GET /goals and the
reader's Goals page (docs/goals.md): one problem, many
approaches, and a catalog that keeps ECC2K-130 from being funded under two
names.
Working from a source checkout, build with the ui feature first (see
Install above — make ui-build, then ./target/release/cairn run); the
default cargo build does not embed the reader and cairn run says so.
Published release tarballs contain a single cairn binary. They are built
with the UI feature by the release workflow and are checked by starting
cairn run, completing an MCP handshake, and fetching /ui/ from the same
unpacked binary.
cargo test # the full suite, loopback only
./scripts/demo.sh # objectives, commit-reveal, audit, attribution
./scripts/ratchet-demo.sh # progressive bounty: publishing beats hoarding
./scripts/try-demo.sh # one round in one command, and a scaffolded challenge
./scripts/interop.sh # each implementation audits the other's log
./scripts/mcp-smoke.sh # the MCP server, driven as a real process
./scripts/blob-demo.sh # a node with only the log fetches its verifier and uses it
./scripts/p2p-demo.sh # two daemons: an empty node syncs, then audits under both
cairn incentives # evaluate the node-operator game (~2s)
cairn incentives --sweep canary-rate=1/20..1/5:5 --out grid.csv
# ...across a grid instead of at one point, one row per
# point, so a threshold is visible without re-reading
# a hundred reports by eye
cairn incentives --robustness # ...and how far each parameter can move before it
# breaks. Seventeen parameters walked out along a
# twelve-rung ladder in both directions, the whole
# mechanism re-evaluated at every rung: about a minute
# and a half on an Apple M4 Pro, a few minutes on slower
# hardware, with progress on stderr so stdout stays the
# report.
On Linux, install bubblewrap
(apt install bubblewrap) so objective-authored verifier code runs inside an
OS jail, and set CAIRN_REQUIRE_SANDBOX=1 on any node that verifies
objectives it did not write — without a jail mechanism the code runs
unconfined, and that variable turns "unconfined" into Unavailable instead.
launch/ holds a real settled log, the checkpoint signing it, and the public
key — so the claim above can be tested before you build anything of your own:
cairn --log launch/cairn.jsonl --root . audit
cairn --log launch/cairn.jsonl --root . verify \
--from launch/checkpoint.json --root-key launch/root-key.pub --audit
# and the check that matters: the independent implementation re-deriving it
./reference/rust/target/release/cairn-reference \
--log launch/cairn.jsonl --root . audit
Both print the same Merkle root. See launch/README.md for what is in it and the two caveats that come with a sample artifact.
Checking one entry does not need the log at all:
cairn --log launch/cairn.jsonl prove 12 --out proof.json
cairn check proof.json --from launch/checkpoint.json \
--root-key launch/root-key.pub
check opens no log — the proof and the signed root are the whole input, which
is what makes it something a light client can run. Five hashes here; fifteen for
a log of twenty thousand.
cairn --log cairn.jsonl --root . serve --listen 0.0.0.0:8080
GET /log returns the log byte for byte; GET /objectives and
GET /frontier/{id} are conveniences over it. A contributor fetches it and
re-derives everything themselves with cairn verify --from, which is the
whole point — they need not trust the server that served it.
Add --queue ./queue to accept POST /submit — objectives, commitments and
claims alike. Submissions are queued, never appended: the operator's node
admits them, re-checking every rule against the whole log. The embedded reader
at /ui/submit posts an objective from a form and lets an Ed25519 browser
wallet (Phantom, Solflare, Backpack) sign its funding authorization; the node
supplies the canonical bytes at POST /objective/prepare so no encoding rule is
re-implemented in a browser. See serving.md.
Which raises the question of who admits them — a Ledger has one writer, so a
publisher on its own can only ever queue. The answer is usually not a second
process:
cairn --log cairn.jsonl --root . p2p \
--identity id.json --root-key root.key --checkpoint cp.json \
--listen 0.0.0.0:9000 --queue ./queue --serve 0.0.0.0:8080
--serve puts the HTTP server on a thread beside the sync loop, against the
same log. Because that process is the one holding the write lock, it drains the
queue it fills: a submission arriving over HTTP is admitted on the next tick,
with nobody running cairn drain at all.
cairn serve --p2p-listen 0.0.0.0:9000 --identity … --root-key … --checkpoint …
is the same daemon entered from the other side — both subcommands call
cairn::daemon::run, so there is one implementation of "be a node" rather
than one per subcommand. make node runs it with the repository's defaults, and
./scripts/node-smoke.sh is the test that a submission really does get admitted
by the process that received it.
The split topology still works and is still right for a read-only mirror:
cairn serve alone takes no lock and holds no Node, so it is safe to
point at a log something else is writing. What it cannot do is admit anything.
There are two roles and they run different commands. Nearly everyone wants the first.
Joining — dial out, serve nobody.
make p2p
Binds 127.0.0.1:9000 (nothing needs to dial you), writes
.local/cairn-p2p.jsonl, and on first run creates
.local/node.identity.json, .local/root.key, and .local/checkpoint.json.
The first two are private keys; .local/ is gitignored, keep it that way.
make node runs the same daemon with --serve also on, so peers, the HTTP
publisher and (if built with the ui feature) the reader all come up from one
process against one log, as described above.
Get a real bootstrap first.
make seeds
Downloads the seed list this project publishes at
https://aburan28.github.io/cairn/seeds.json, checks each
entry against the transport key beside it, and writes one bootstrap file per
verified seed into .local/seeds/. make p2p and make node then use those
instead of the placeholder below, automatically. Re-run it whenever you like: a
seed that moves publishes a new address under the same key, and this is how you
pick that up.
Nothing about that fetch has to be trusted, which is the point of putting it on
a host this project does not control. A peer id is sha256 of the McEliece
transport key, so cairn seeds resolve re-derives it from the bytes before it
writes anything: a hostile mirror can withhold a seed, or serve a key that does
not match its name — both refused — and cannot serve a different key under the
same name. The worst it can do is send you to a machine of its choosing, which
then fails the handshake, and costs you one dial. Point it elsewhere if you
prefer — a fork, a mirror, a file:// copy — since none of them is privileged:
make seeds SEEDS_URL=https://example.invalid/seeds.json
And if nothing verifies, one thing will stop this working, and it is not the
network. With no verified seed and no explicit BOOTSTRAP_ARGS, the first run
falls back to generating .local/seed.json for SEED_ADDR with a
placeholder public key — a real key, freshly minted, that belongs to nobody.
The address is only ever a dial hint; p2p::handshake authenticates the key,
so a placeholder authenticates nobody and every handshake fails. Until you paste
the seed's real key into "public", the daemon says so at startup:
bootstrap .local/seed.json: still carries the PLACEHOLDER key ...
That warning clears itself once the key is real — it is keyed on the peer id of the generated key, not on a flag anyone has to remember to delete. Point somewhere else instead if you prefer:
make p2p SEED_ADDR=203.0.113.9:9001 # regenerate the hint for another host
make p2p BOOTSTRAP_ARGS='--bootstrap peer.json' # or supply the file outright
A bootstrap file is just {"addr":"host:port","public":"<peer public-key hex>"}.
addr may be a hostname, which survives a restart that moves an IP.
Seeding — accept strangers.
make seed
Binds 0.0.0.0 on SEED_ADDR's port (the port is derived from SEED_ADDR, so
the two cannot disagree). Binding the wildcard is the counterintuitive part and
the one that matters on a cloud host: an instance's public address is NAT'd to
it and appears on no local interface, so --listen <public ip> cannot bind at
all. Publish the public address in the bootstrap file you hand out.
Two things make seed cannot do for you, both of which look identical to a
seed that is simply down:
Open the port inbound. A security group that drops rather than refuses produces no error on either end — just silence until a timeout.
Distribute your real key. Hand out the "public" field from your
--identity file (.local/node.identity.json), not your own .local/seed.json,
which holds a placeholder for somebody else. The published way to do that:
cairn seeds publish --identity .local/node.identity.json --out launch/seeds/
It writes <your peer id>.key and prints the entry to add to
launch/seeds.json; open a pull request with both and make seeds finds you.
There is no upload endpoint, deliberately — publishing is a reviewed change to
a repository, which is what makes this anchor replaceable by somebody other
than whoever holds the server. See launch/seeds/README.md.
Is it actually connected? Successful sessions are logged, not just failures:
2026-08-08T00:33:42+00:00 INFO cairn inbound session: <peer id> ok, 12 entries now
2026-08-08T00:33:42+00:00 INFO cairn outbound session: <peer id> ok, 12 entries now
Silence with neither those nor an error means nothing has been dialled yet;
outbound session: transport I/O: ... on repeat means the address, the
firewall, or the key. Check them in that order — the key is the one with no
network symptom.
CAIRN_LOG_LEVEL sets the level — error, warn, info (default), debug,
trace, or off. Everything goes to stderr, always, which is what makes it
safe to raise on cairn mcp and cairn run, whose stdout is the JSON-RPC
protocol.
It used to be CAIRN_LOG, and the cairn CLI read that same name as the
ledger path — so CAIRN_LOG=debug exported for a daemon sent cairn audit
to an empty file called debug, which audits clean. The two are now
CAIRN_LOG_LEVEL (the level, every daemon) and CAIRN_LOG_PATH (the ledger,
the CLI). The daemons still accept a level in CAIRN_LOG with a warning; the
CLI refuses one outright and names both variables.
debug adds every p2p protocol message, in both directions, with the peer it
was exchanged with:
$ CAIRN_LOG_LEVEL=debug make p2p
… DEBUG cairn::p2p 59758322… -> Hello peer=083e078e… records=0
… DEBUG cairn::p2p 59758322… <- Inventory records=1
… DEBUG cairn::p2p 59758322… -> Want ids=1
… DEBUG cairn::p2p 59758322… <- Records n=1
… DEBUG cairn::p2p 59758322… -> code.Want addresses=1
… DEBUG cairn::p2p 59758322… -> dht.Ask addresses=1
That is a whole anti-entropy round: records, then verifier code, then the DHT, then populations — the order p2p.md describes, now observable rather than inferred. Counts and sizes, never contents: a record round moves whole claims, and a population round would otherwise print other people's candidate artifacts into your terminal.
Use separate LOCAL_DIR, IDENTITY, ROOT_KEY, and CHECKPOINT paths for each
node. make mcp uses .local/cairn-mcp.jsonl by default, and make p2p
uses .local/cairn-p2p.jsonl, so the two commands can run together without
contention over a single hash-linked ledger. Use P2P_LOG and MCP_LOG to
override these paths directly.
The root checkpoint key is ML-DSA-65 and is separate from the transport identity.
Rust 1.89+ (verified in CI, and asserted by rust-version). No network access
needed at runtime.
An objective is a funded question that comes with a runnable verifier, pinned by hash:
This is examples/capset/objective.json
verbatim — every value real, so it posts as it stands:
{
"goal": "GOAL-capset-lower-bounds",
"statement": "Exhibit a cap set in F_3^4 of size at least 20 (no three distinct points collinear). Score is the set size; the maximum is known to be 20.",
"verifier": {
"kind": "evaluator",
"evaluator": "examples/capset/evaluators/cap_set.py",
"evaluator_sha256": "05ad14fa10bd3055a8f3b1962a6a909832887676558035e40e44c4bd0aa271c4",
"entrypoint": "score",
"threshold": 20,
"direction": "maximize"
},
"reward": 250000,
"funder": "treasury",
"created_at": "2026-07-28T00:00:00+00:00"
}
The hash is the real one, and abbreviating it would not be a tidier example —
it would be a broken one. A wrong pin does not fail: the id covers the
verifier, so it mints a different objective, one whose every claim returns
InvalidSpec forever and whose reward is stranded. post warns when a pin
does not resolve locally but cannot refuse, because posting an objective whose
checker a peer will serve is exactly how content-addressed distribution works.
Key order does not matter here — the record is canonicalized before hashing —
but the bytes of every value do.
An objective's id is the hash of that whole record, verifier included. There is no operation that changes the rules of a funded bounty — editing the evaluator produces a different objective and the claims against the original stop resolving. Mid-bounty rule changes aren't guarded against; they're unrepresentable.
cairn post examples/capset/objective.json
# In a scarce-supply log, sign the objective with the funding identity:
cairn post examples/capset/objective.json --identity treasury.json
cairn commit <objective-id> --submitter bob --artifact solution.json --nonce s3cret
cairn reveal <objective-id> --submitter bob --artifact solution.json --nonce s3cret
cairn try examples/capset/objective.json --submitter bob --artifact solution.json
# the three lines above in one, waiting out
# the epoch between commit and reveal
cairn scaffold my-challenge --kind certificate # the files a new objective starts from
cairn audit
cairn attribute
cairn checkpoint --root-key key.json --out checkpoint.json # sign it
cairn prove 12 --out proof.json # one entry, provably
cairn check proof.json --from checkpoint.json # ... checked without the log
cairn drain --queue ./queue # admit what arrived over HTTP
| kind | checks | cost | trusts |
|---|---|---|---|
certificate | recomputes an NP witness | ms | nothing |
evaluator | scores a candidate against a pinned fitness function | 1 evaluation | evaluator is pinned and pure |
statistical | re-runs a pinned test statistic at a pinned seed against a pre-registered threshold | 1 run of the statistic | statistic is pinned and pure; the criterion was fixed before the data |
lean | a proof assistant kernel accepts the proof | seconds | kernel soundness |
replay | re-runs a pinned computation, compares declared fields | full re-run | bit-reproducibility |
Pinned verifier code runs as a subprocess inside an OS jail — bubblewrap on
Linux, a seatbelt profile on macOS — with its hash checked first: no network,
writes confined to a scratch directory, a wall-clock deadline. Not a VM
boundary; docs/verification.md#sandboxing names the gaps that remain. The
lean verifier rejects sorry, admit, new axioms, and native_decide
before Lean ever runs, because each produces a file the kernel accepts while
proving nothing.
Unavailable — never Reject. A
missing toolchain, a crashed checker, or a timeout is an infrastructure fact,
not a fact about the artifact. Collapsing it into a rejection turns "my Lean
install is broken" into "your proof is wrong", and hands an attacker a way to
fail every honest submission by taking verifiers offline. Only Accept and
Reject settle anything.canonical::Value has no
float variant, so an object whose identity could differ between two honest
nodes cannot be constructed. IEEE-754 doubles don't round-trip identically
through every JSON implementation and don't reproduce bitwise across
heterogeneous hardware.reward * progress overflows u64 at
realistic values; every such path uses u128 intermediates and returns an
error rather than wrapping, with overflow-checks on in release too.replay refuses to treat wall-clock, memory, or
FLOPs as a checkable field — those measure the host, not the computation.refutes, replicates, supersedes, retracts — and
attribution, settlement and the frontier read cites and none of them read
those. If declaring "X is wrong" could shift a payout, refutation would be a
way to bill X. Relations feed a derived view instead; see
knowledge.md.Thousands of participants on one objective must avoid duplicating each other and share what they find. That's usually attacked with machinery — dispatchers, reservations, locks. Most of it is self-inflicted: a winner-take-all bounty gives everyone a reason to hoard, so nobody shares and everyone rediscovers the same partial results.
frontier.rs changes the payment structure instead. An objective carries a
monotone best-known score; whoever moves it is paid for the distance moved.
Payouts telescope, so the pool is exactly exhausted at the target however the
curve is chopped.
alice: 12-point cap set reward 300000
eve: copies alice reward 0 (does not improve)
bob: 16, citing alice reward 400000
carol: 20, citing bob reward 400000 (pool exhausted)
after citation flow: alice 442857 · bob 357143 · carol 300000
Alice ends up with the largest total from the smallest direct reward, because two people built on her. Publishing immediately becomes the profitable move, copying earns zero, and an improvement must cite the frontier it beat — enforced at submission, so attribution needs no judgement.
Flow is weighted by each cited claim's settled reward — which on a ratchet is the progress it moved — rather than decaying by citation depth. That is what stops an improver chopping one advance into many steps to dilute the person below them: a later contributor pays alice the same however bob packaged his work, and slicing converges to a small premium instead of draining her. See the design note.
| state | volume | needs | mechanism |
|---|---|---|---|
| frontier — who holds the best score | low | total order | consensus |
| population — candidates worth mutating | high | eventual convergence | CRDT + gossip |
| work split — which region a node searches | zero messages | nothing | pure function |
The population is a bounded join-semilattice: merge is commutative, associative
and idempotent, so nodes converge with no rounds and no leader. Divergence is not
a bug — it's the island model preserving search diversity. Gossip is
untrusted: a peer asserting score = 10^12 would evict every real candidate, so
ingest() re-scores locally and drops what doesn't reproduce.
A research program has a fourth kind of state that never touches the network:
its working records, task leases, messages between agents and run outputs,
written by many agents at once and often offline. cairn lab keeps it as a set
of signed ops that merge like a CRDT — no git, no server, every concurrent edit
a visible conflict instead of a merge-time surprise, immutable records that
cannot be replaced — and runs experiments in content-addressed root filesystems
(SageMath, PARI/GP, msolve) under gVisor, recording each run's receipt and
outputs as one op. It settles nothing; see docs/lab.md.
For live source editing, collaboration workspaces
add Unicode text CRDTs, isolated task lanes, explicit causal integration and
immutable experiment checkpoints. CLI and MCP agents use the same protocol;
continuous encrypted sync carries edits between replicas without Git.
cairn lab web --identity author.json opens a local graphical workbench for
protected drafts, explicit conflict resolution, pinned experiments and ISA-aware
compute estimates. Generation rollover
starts a fresh text graph while retaining a verifiable archive of the old one.
What all of this looks like while it runs is the reader's Network and
Coordination pages (/ui/network, /ui/coordination?id=…): whom this
node has actually reached (GET /sessions, from the daemon's own memory rather
than the log's address book), what hardware is heartbeating to it and at what
summed rate, who holds which slice of a divided search this epoch, and the
advisory leases workers post to say what they are about to work
(POST /lease). A lease is not a lock — the partition function still assigns
every unit and nothing that pays reads one — and a node's declared roles
(CAIRN_ROLES: coordinator, executor, verifier, relay) are a hint about intent
shown beside what the log shows identities doing, never a permission. The
design, and what it deliberately leaves out of consensus, is
docs/design/network-coordination.md.
The ratchet pays for moving a score. Some problems have no score to move —
only a great many small results, each checkable on its own, and an answer
that falls out once enough of them exist. A distributed Pollard rho is the
canonical one: a distinguished point (x, y, a, b) with a·P + b·Q = (x, y)
costs 2^d group operations to produce and two scalar multiplications to
check, and the search is the table of them.
A coordinator posts one objective with a piecework block, and every node
learns of it from the log:
{
"statement": "Contribute distinguished points to the shared rho search ...",
"verifier": { "kind": "certificate", "checker": "examples/certicom-ecdlp/checkers/nums_50_rho_dp.py", ... },
"piecework": { "unit_price": 100, "units": 281474976710656 },
"reward": 80000
}
Peers ask work_assignment for their share and get it as a range of the
coordinator's units — the same coordinator-free slice as any other objective,
mapped onto unit indices. Each novel accepted unit pays unit_price from
the pool; a unit already paid mints nothing; a rejected answer consumes
nothing; and the objective is closed when the pool is empty, not before. A
top-up is another objective with the same checker and the same block, and it
inherits the first one's paid units.
alice: point from walker 0 reward 100
bob: point from walker 1 reward 100
eve: copies alice's point reward 0 (duplicate unit mints nothing)
carol: point from walker 2 reward 100 (pool: 79700 of 80000 remaining)
At scale a claim is a batch: the block names the artifact field holding
an array of units ("items": "dps") and the fields that identify one
("key": ["x", "y", "a", "b"]), so a claim pays per novel element and an
element's provenance -- the walker index and step count an auditor re-walks
-- does not make a paid point new.
scripts/piecework-demo.sh runs this on the 50-bit instance with a
pure-Python walker (examples/certicom-ecdlp/tools/rho_dp.py), single points
and then a batch, then has both implementations audit the log and the walker
re-walk a sample of the batch. The design is
docs/design/rho-piecework.md.
Every payment above points the same way: a funder escrows, an artifact verifies, settlement releases, citation flow moves a fraction of that same money backwards. Every unit that reaches a participant entered as somebody's bounty. Nothing pays an agent for something another agent wanted — a decomposition, a sub-frontier candidate, a branch somebody else explored.
The scope for closing that is agent-market.md, whose
central question — does pricing sub-frontier candidates starve the gossip
population the search runs on? — is now solved rather than argued.
cairn incentives --market plays it out: the commons survives, universal
gossip is a strict equilibrium, and so is universal selling. The barriers between
them are not symmetric, and the part nobody had guessed is that which way they
lean is set by how leaky the gossip transport is. At a hundredth withheld, 28
sellers break a 200-agent commons and 174 gossipers are needed to recover it; at
a fifth the same measurement reverses to 151 against 51.
Its other conclusion is that the mechanism is already here. Objective::funder is a
string, there is no balance and no transfer primitive anywhere in src/, and an
agent-to-agent payment is best expressed as an objective rather than a transfer:
escrow, verification, settlement, audit and citation flow then apply unchanged,
and fair exchange falls out instead of needing its own protocol.
What makes it tractable at all is an asymmetry that inverts the verifier's dilemma. Nobody holds the right answer about an artifact, which is why verification needs canaries — but
the buyer is the oracle. An agent spending its own money on a good it wants is motivated to price it correctly, so the protocol enforces atomicity and never valuation.
That survives exactly one rule: no protocol payment may ever be a function of trade volume. A sybil pair trades at any price for free, so a fee rebate or a reputation that pays is the grinding attack with a market around it.
Three results the scope pins down:
π < Δ + φ and selling is dominated for anyone
with standing to move the frontier. The market's whole domain is the goods the
ratchet prices at zero — which is a boundary the payoffs already draw rather
than one anybody has to enforce.And one risk that decides whether it is worth building at all: candidates currently circulate through gossip because nothing prices them. Price them and gossiping is giving away inventory, so a market for candidates may starve the population the island model runs on. That is a payoff question, it is the highest- value item in the scope, and it belongs in the harness before it belongs in code.
Everything above pays submitters. Nothing in it pays the machines that re-run the verifiers, hold the log, or custody the shares that open a sealed submission — all public goods, all of which the dominant strategy is to leave to somebody else.
The hard one is verification, and it is hard structurally rather than quantitatively. Punish a node for accepting work that somebody else later proves invalid, and
"everybody rubber-stamps" is a Nash equilibrium at any penalty — if nobody checks, nobody is caught, so no penalty ever fires.
Raising the slash does not touch it. The mechanism has to manufacture its own ground truth: canaries, artifacts whose verdict the protocol already knows, mixed indistinguishably into each node's sample. Then the punishment is unconditional and the equilibrium moves.
Availability and custody need no such trick, and the reason is the whole design in one line: the protocol already holds the right answer. A Merkle challenge is checked against a published root; a share that never appears names its holder. Verification is the only service with no oracle.
src/incentive/ is the mechanism and a harness that evaluates it — exact
rational payoffs (no floats, so "is this an equilibrium" is decidable), the full
ladder from individual rationality up to k-resilience and sybil-proofness, and
better-reply dynamics for where a population lands rather than where it could
rest.
And the verdict is a point, which is the weakest form of the claim. --robustness
walks each parameter outward until the report stops passing, in both directions —
fraud_rate breaks the mechanism by getting smaller, so a one-directional
search would call it safe. The result contradicts where the prose spends its
attention: the four tightest constraints are all custody. Verification, the
sub-game with the interesting structural argument, survives a sixteenfold error
in the cost of a check; custody does not survive a quarter. No passing verdict
would have said so. See proving-it.md for what that does
and does not establish.
$ cairn incentives --canary-rate 0
verification -- honest action: verify
honest profile strict Nash ok
pure equilibria 2
rival (strict) equilibria 1 FAIL
smallest defection 100 nodes FAIL
free (zero-gain) drift none ok
tipping point 100 nodes FAIL
binding constraint canary_rate must exceed 1/1425 (currently 0)
Three results worth stating plainly, each pinned by a test:
Also stated rather than papered over: a committee member standing ready to collude is behaviourally identical to an honest one, so the custody equilibrium is weak at every parameter set and no bond makes it strict. What the bond buys is that reaching the threshold does not pay.
Where a node's data lives is the operator's choice, and what leaks off their disk is their risk. Four things, one command each:
cairn keygen # 32-byte key at ~/.cairn/key, 0600
cairn --data-dir /Volumes/ext/pw audit # data wherever you want it
cairn --data-dir /Volumes/ext/pw --max-size 20GB store gc
cairn --data-dir /Volumes/ext/pw sync ~/Dropbox/pw-backup
cairn --data-dir /Volumes/ext/pw store rekey # new key, same root, no plaintext on disk
cairn --data-dir /Volumes/ext/pw store export --out public.jsonl
rekey is the one worth a sentence. It re-seals every line under a fresh key and
requires the new file to re-derive the same entries and the same Merkle root
before anything is swapped. It keeps the old key at <key>.previous — a mirror
you made last month is still sealed under it — and does not keep the old
ciphertext, which would otherwise sit there readable by the key you are retiring.
export is the inverse of store encrypt, and it exists because sealing a store
must not be a one-way door out of the claim at the top of this file. Without it,
an operator who encrypted their own copy could no longer produce the readable log
anyone else would audit.
src/p2p/swarm/: piece-level transfer and a DHTTwo things here that the rest of src/p2p/ does not have, and one honest
overlap.
It sat at src/swarm/, beside p2p rather than inside it, for most of its
life. The move settles a cycle rather than a matter of taste: tcp::KeySource
is declared here and implemented by p2p::service::Service, so as siblings each
module named the other and which way the dependency ran was something a reader
had to reconstruct. Blob transfer consumes the transport, the sessions and the
key distribution, and nothing there consumes it back.
A Kademlia DHT (src/p2p/swarm/dht.rs) for the question a fetch actually asks:
who holds digest D right now. p2p::discovery answers which peers exist;
without a provider lookup, finding a blob means asking everyone. XOR metric,
k-buckets, provider records with expiry, and the α-parallel iterative lookup as a
pure state machine — convergence and termination asserted against a synthetic
200-node network built from real routing tables.
The k-bucket policy is the part worth reading twice, because it is backwards from
every cache written by reflex: when a bucket is full the oldest still-live
contact wins and the newcomer is discarded. Longevity is the one thing an
attacker flooding fresh identities cannot manufacture. So insert does not
evict — it returns the contact to probe and lets the caller decide after
actually trying it, which keeps the policy testable without a network.
DHTs earned a bad security reputation honestly, and it does not transfer here:
Every DHT answer is a hint. The digest decides.
A lying provider record costs one wasted dial, checked against a digest the log fixed before the lookup started. Eclipse costs liveness, never correctness. Node IDs are hashes of ed25519 public keys, so claiming one costs a keypair and a signature — S/Kademlia's crypto-ID mitigation, free from the identity layer.
Piece-level transfer (piece.rs, wire.rs) — manifests of piece hashes,
bitfields, rarest-first, bounded pipelining, tit-for-tat choking, endgame with
cancels. Piece hashes buy not trust (anyone can compute a manifest) but blame:
a bad piece is localised to one peer instead of costing the whole download. And
rarest-first is really a durability rule rather than a throughput trick — it
prevents the last-copy state the availability mechanism pays to avoid.
The overlap, and what was done about it. The DHT is no longer duplicated:
the metric, the k-buckets, the iterative lookup and the provider store live in
src/dht.rs, generic over a contact type, and swarm::dht and p2p::dht are
both instantiations of it. p2p::dht is the one the daemon runs: a session
asks which of the blobs this node wants the peer holds, and Service::peers_for
then asks a peer that said yes instead of asking three at random.
Asks, not announces, and that is the interesting constraint. p2p::code already
refuses to offer an inventory of held blobs, because that list is a list of the
objectives a node is working on. Building a DHT by publishing it would spend
exactly the privacy code declined to spend, so holdership is pulled: a node
answers only for addresses the peer named, and the set it names is the
code_want already sent on that connection. The round adds routing knowledge at
no additional disclosure.
Two details that are specific rather than incidental. A PeerId is already
sha256(McEliece public key), so it is the Kademlia id — identity is
self-certifying through the handshake, with no signature needed. And a contact
deliberately does not carry the key, because that key is 261,120 bytes and a
full routing table holding one per contact would cost about 1.3 GB; the key
comes from the address book at dial time, which is why a p2p routing answer is
a hint checked by dialling rather than a proof.
Everything that opens a socket goes over the encrypted transport. Records,
code, DHT, populations — and now swarm::tcp, which spent a while in plaintext
behind an off-by-default feature because swarm's peer records named an ed25519
identity while the transport needs a 261,120-byte McEliece key. Giving the
record the 32-byte id of one settled that, and the feature gate is gone.
Neither claim is asserted in a comment: tests/wire_encryption.rs and
a_transfer_puts_no_plaintext_on_the_wire each put a recording relay between
two real nodes and check the captured bytes.
swarm::tcp is now driven by cairn blob serve and cairn blob fetch,
and scripts/blob-demo.sh runs both sides in CI: a node holding only the log
fetches its pinned verifier from a stranger and settles a claim with it. That
was worth doing for its own sake and it also found two bugs that no unit test on
either side could reach, because the module had no caller in any shipped
binary and so had only ever been checked against itself. See
storage.md.
The two address books look like a duplicate and are not, which took reading
both to establish. p2p::discovery::AddressBook maps a transport id to an
endpoint — an address and the 261,120-byte McEliece key needed to dial it. It is a
local key cache and nothing about it is relayable. swarm::discovery holds
signed peer records — ed25519 identity, addresses, a monotonic sequence — and
exists to be handed to strangers: offer and share are peer exchange, with
bounds and persistence. One says how to open a session, the other says who a
peer is and where it claims to be, verifiably. swarm::tcp::KeySource is the
join between them, and it is what lets an address learned by asking become a
dial.
What was duplicated — two transports — is gone: swarm::tcp runs over
p2p::transport like everything else here.
The real remaining gap was narrower and is closed: p2p had the signed
sequence available and was throwing it away. seed_from_log reads peer
records out of the log, where Node::peers has already resolved
highest-seq-wins and the audit reports one that fails to advance — and then
handed the routing table a hardcoded zero. Since the table takes a contact when
seq >= held, at zero a replayed record always won, so anyone who had once seen
a peer record could steer traffic back to an address that peer had left. See
discovery.md for the design and the survey, including why
encrypted DNS answers a different question than the one people ask it.
One caveat that argues against the whole module: blobs::MAX_BLOB_BYTES caps a
blob at 1 MiB, which is four pieces. At that size rarest-first and choking buy
nothing, and p2p::code's whole-blob transfer is the right call. The swarm
machinery is sized for a constraint this design does not currently have.
src/shards/: holding part of a blob, and proving you hold itEvery path above moves a whole blob, so surviving f holders vanishing costs
f + 1 full copies. src/shards/ splits it instead: k data
shards plus m parity, any k of which rebuild it, at (k+m)/k on disk —
1.5× at (4, 2) where replication wants 3× for the same two tolerated losses.
The reason this is not just a compression trick is what coding costs.
Replication has a property so cheap nobody names it: every copy is
self-checking, so a liar implicates only itself. A shard does not hash to the
blob's digest, and one corrupt shard makes every output byte wrong — the digest
then says somebody lied and nothing about who, leaving n choose k full decodes
to find them. So each shard is cut into chunks, each chunk is committed under a
per-shard Merkle root, and the shard roots under one manifest root; nothing
enters the linear combination until it has been checked, and a liar is dropped
and named.
Systematic Cauchy Reed–Solomon over GF(2^8) — now the crate's only GF(2^8), shared with Shamir, with the bulk lookup table built from the branch-free multiply rather than written beside it. Two multiplies disagreeing on one of 65,536 products would produce shards that reconstruct to garbage on the node that used the other one.
No record kind and no transfer path, both deliberately: a manifest is derived
from bytes and is checked against the digest the log already pinned, and
cairn shard is the caller that keeps the module honest until swarm grows
one. See shards.md.
Assume censorship. But separate four properties that get bundled under "encrypt it", because encryption delivers only one of them:
| property | mechanism |
|---|---|
| confidentiality — observers can't read | encryption |
| unlinkability — observers can't tell who | pseudonyms, ZK |
| censorship resistance — your submission gets included | forced/blind inclusion |
| availability — content can't be withheld | replication, gossip |
Encrypting settled artifacts would destroy the project. Public verifiability requires them readable; encrypt them and you're back to trusting an operator.
The real censorship hole is elsewhere: commit–reveal requires the submitter to act twice. An adversary who can't forge or steal your work can still take it by stopping the second action — a DoS, a network block, a detention, or a sequencer that drops your reveal until the deadline passes.
So submissions are sealed, and opened without the submitter:
commit commitment = H(artifact ‖ submitter ‖ nonce) (unchanged)
envelope = ChaCha20-Poly1305(K, the whole signed claim)
shares = Pedersen VSS(K, 3-of-5), each sealed by post-quantum KEM,
with the dealer's commitments, so every share is checkable
epoch end ≥3 seats publish `committee_share` records → anyone opens it
a seat whose share does not open complains; the dealer answers with
that share in the clear, or the submission is disqualified
You can be offline, jailed, or firewalled and still be paid. It also kills in-flight front-running, and makes selective censorship visible — a sequencer can't see what it's dropping, so it must include everything or censor indiscriminately. The commitment binds the plaintext, so a submitter who seals garbage is caught the moment the committee opens it.
Sealing moves when an artifact becomes public, never whether.
And who opens it, and when, are consensus rules rather than promises. The
committee is a beacon draw over the log's own peer records — nobody issues an
invitation, so nobody can decline to send one, and any reader recomputes any
seat. A share published before the commitment's epoch closes is refused, and
that check compares two timestamps already in the log rather than consulting a
clock, so a member with a fast clock writes a record every node rejects. The
threshold is pinned at commit time against the network's constant, because a
submitter who could seal one-of-five would be handing any single member an
early read. tests/committee_reveal.rs runs the whole thing with the submitter
gone after commit.
Two things are stated rather than papered over: citation flow requires linkage — the pseudonym graph is public by construction because paying people for being built upon is what it's for — and encryption does not stop a sequencer that includes nothing, which needs forced inclusion on a base layer.
For a pure pinned verifier, correctness is not a consensus question — anyone re-runs the checker and gets the same answer. What needs agreement is narrower: ordering (who advanced the frontier first) and data availability (was this published, or withheld).
That inverts the usual priorities. Throughput barely matters; frontier advances are minutes apart. Censorship resistance matters enormously, because withholding a competitor's reveal steals a bounty, and liveness is money.
So: don't write a consensus protocol, and don't run an L1. Use a rollup on an
established chain — the bootstrap circularity (stake value ← research ← chain)
has no starting point, and forced inclusion via a base layer delivers the primary
security property directly. The state transition is already the pure function in
node.rs, and audit() is already the re-derivation a fraud proof needs.
A verdict is not the end of a claim's life — results get replicated, superseded, narrowed, retracted — so the log records those as typed edges too, and anyone derives a claim's standing and a confidence number from them. The rules that keep this from becoming the vote the section above refuses:
cairn knowledge <claim> exposes every knob.refuted, and no quantity of claims saying otherwise moves it.src/ Rust implementation (primary)
canonical.rs content addressing; the cross-implementation contract
records.rs Objective / Commitment / Claim
ledger.rs hash-linked append-only log
node.rs the rules engine and the audit
frontier.rs progressive bounties
attribution.rs recursive citation flow
knowledge.rs standing and confidence, derived — never stored, never paid
gossip.rs the candidate population CRDT
partition.rs coordinator-free work assignment
verifiers/ certificate, evaluator, lean, replay
crypto/ Shamir, sealed envelopes, pseudonymous identity
sealed.rs sealed submissions, openable without the submitter
incentive/ the node-operator mechanism, and the harness that evaluates it
store/ at-rest encryption, the data directory, the size cap, the mirror
shards/ erasure coding, with a Merkle commitment per chunk
swarm/ piece-level transfer and a Kademlia DHT, alongside p2p/
lab/ the research workspace: signed op CRDT, sync, gVisor runs
conformance/ cross-implementation vectors — the binding contract
docs/ the design notes
examples/ worked objectives with real artifacts
docs/README.md is the map — every page below, grouped by what you are trying to do, with the design notes marked built or not built. The five a user reaches for first:
The design, in the order the ideas depend on each other:
CAIRN_REQUIRE_SANDBOX=1 is set.
VM-class isolation is Stage 2; see the threat model before opening
objective authorship to strangers.src/incentive/ is a mechanism and
its evaluation, not a code path. The verification half of it now is one:
src/canary.rs mints submissions whose verdict it established by running the
objective's own pinned verifier, records::Attestation puts a signed 50,000-
unit bond behind what an operator says a verifier returned, and
cairn attest slash --docket takes that bond — the naming costs a map
lookup, the taking costs one verifier run, and docs/bonded-verification.md
has the numbers. What is still only a model is the rest: no Merkle
availability challenge is issued, no committee seat is bonded, and nothing
requires an attestation at all — what is priced is lying, not silence.FunSearch / AlphaEvolve for propose-and-evaluate, the Equational Theories Project for crowdsourced kernel-verified mathematics, and INTELLECT-2 / TOPLOC for verifying permissionlessly contributed inference across non-deterministic GPUs.
Apache-2.0