Skip to content

Repository files navigation

ALIBI — Proof-of-Refusal

Prove your agent refused an order — without revealing the order, the leash, or where the leash sits.

A privacy-first dApp on Midnight. Built for THE MIDNIGHT BUILDATHON (AKINDO), Wave 1.

⚠️ Status: scaffold. The Compact circuits are specified but their bodies are unwritten, and the UI is route stubs. Nothing here has been through compactc yet. This README documents the design and the build plan; it will be rewritten as an evaluation guide before submission. Do not read any claim below as "implemented" unless the judge verification section says so.


The problem

Nadia runs a two-person crypto treasury desk. Her rebalancing agent runs unattended overnight against three venues. At 02:14 it parses an inbound message containing an injected instruction — transfer 40,000 USDC to an address whose counterparty class her mandate does not cover, at an amount above her epoch cap.

It refuses. And she cannot prove it refused.

Her auditor sees an absence of a transaction, which is indistinguishable from the agent having been down. She cannot hand over the log, because it contains the desk's entire venue strategy and the injected payload names a real counterparty. Her actual workaround today is a Notion page saying "trust me, it held." That page is worth nothing.

Every agent-authorization system can prove an agent was allowed to act. ALIBI proves the other half: that an agent was asked, and declined — publishable to anyone, revealing nothing.

Why zero-knowledge is load-bearing here, not decorative

Refusal evidence is already being standardized. IETF draft-sabey-refusal-transparency-00 specifies it as signed plaintext with a mandatory refusal_ground, and draft-kamimura-scitt-refusal-events maps refusal decisions onto SCITT signed statements. (Both are Internet-Drafts. Neither is WG-adopted.)

Publishing the ground publishes the boundary. Each published refusal is one negative sample of the policy surface, so an attacker who can trigger refusals binary-searches the cap in O(log n) probes. Refusal transparency as currently drafted is an oracle for the thing it protects.

ZK is what lets you have the count without the oracle.

Delete any one leg and it collapses:

Delete What breaks
the agent Humans don't publish evidence they declined. Human authority is presumed; machine authority must be presented. The hijack question — was that my delegate's intent, or an injected instruction? — has no human counterpart.
ZK Publishing the refusal ground is self-defeating, per above.
Midnight The count must be public, monotonic and un-rewritable by the agent's own operator, while the mandate and the instruction never leave the machine. Public-tamper-proof-counter over private-unrevealed-content is the dual-ledger model.

Architecture

No backend. No database. State lives in exactly two places, and that split is the product:

PUBLIC LEDGER  (indexerPublicDataProvider)      LOCAL LEVELDB  (levelPrivateStateProvider)
  mandates      agentId → commitment              agentSk, submitterSk
  agentPks      agentId → agentPk                 mandate { capPerEpoch, allowedClasses[8], salt }
  tallyCommit   agentId → H(spent‖epoch‖salt)     spentThisEpoch, tallySalt
  pending       instrCommit → anchored?           instrNonce
  posted        the denominator                   refusalLog [{ instrCommit, reasonCode }]
  executed / refused / outstanding
  nullifiers    anti-replay
  epoch
alibi/
  contract/   Compact source, generated artifacts, simulator tests
  api/        provider wiring, circuit callers, commitment derivation
  ui/         Vite + React + Tailwind + shadcn

The four circuits

# Circuit What it asserts
1 registerMandate One mandate per agent. Only H(cap ‖ classes ‖ agentPk ‖ salt) ever reaches the chain.
2 submitInstruction A third party anchors an instruction before the agent may act on it — proving H(submitterSk) != agentPks[agentId].
3 authorizeAction Anchor is pending · nullifier unused · mandate, tally and instruction commitments all re-derived and matched · class ∈ allowed · spent + amount <= cap.
4 recordRefusal (a)–(e) identical to #3, then the disjunction: (class ∉ allowed) || (spent + amount > cap).

The disjunction is the design. It proves a violation occurred while revealing which rule fired to nobody — including the verifier. Both branches collapse to a single bit. A reasonCode would have been the obvious thing to publish, and it is precisely what leaks the boundary; it stays in local state.

Untrusted witnesses

Midnight's own guidance: "Do not assume in your contract that the code of any witness function is the code that you wrote." Every witness in this contract is hostile until proven otherwise, so circuits 3 and 4 re-derive the mandate commitment, the tally commitment and the instruction commitment, and assert each against on-chain state before anything else happens.

That third one matters more than it looks. Without tallyCommit, spentThisEpoch is an unbound witness — and the agent could satisfy the over-cap branch for any instruction by simply reporting a large tally. The falsifier below would be theatre. Tests N7 and N8 exist to keep it honest.

Conservation

posted == executed + refused + outstanding        (per agent, always)

Deliberately not enforced inside a circuit. It is public-ledger arithmetic that any observer can check against four public counters — which is exactly what makes it worth having. An earlier design proved this over a private witness; that was worthless, because a witness cannot be trusted. outstanding is arithmetically redundant on purpose: tracking it separately turns a definition into a testable invariant, and it is the anti-suppression number — a rising outstanding is what a suppressing agent looks like.

What we claim, stated precisely

Precision here is not modesty; an engineering panel punishes an overclaim far harder than it rewards an ambitious one.

We do not claim "nothing is revealed." A successful recordRefusal publicly discloses that a violation occurred — intended, and the entire point of a public counter. What the disjunction protects is which rule fired.

Exactly one bit is disclosed, by design. The boundary is not.

Two further honest limits:

  • A Midnight transaction names the entry point it invoked. So for a specific anchor, an observer can infer authorizeAction vs recordRefusal. Modelled explicitly, not hidden.
  • Wave 1 attests; it does not enforce. Nothing forces the agent through this contract. The gate is client-side. Wave 2 moves value into contract custody, which is the headline next deliverable. The irony is worth stating first: under full custody enforcement a hijacked agent physically could not make the bad transfer, and proof-of-refusal would degrade toward telemetry.

Threat model. Instructions arriving over the anchored channel are in scope. Unsigned prompt injection from unwitnessed sources is out of scope — if nobody anchored it, there is nothing to prove a refusal against.

No ZKML. We do not prove model inference; that is infeasible on Compact 0.31.0's bounded circuits. We prove policy compliance of actions. The AI is the thing being constrained, never the thing being proven.

Setup

Prerequisites: Node per .nvmrc (24.11.1) · Docker · Chrome (Lace is Chrome-only) · the Compact toolchain.

# 1. Compact toolchain (installs the `compact` manager, then the compiler)
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
export PATH="$HOME/.local/bin:$PATH"   # docs say ~/.compact/bin; it lands in ~/.local/bin
compact update                          # installs compiler 0.31.1
compact compile --version               # → 0.31.1

# 2. Build
nvm use                       # 24.11.1 — the docs say "Node 22+"; the repo pins higher
npm install
npm run compact               # compile Compact → contract/src/managed/   ← THE GATE
npm run proof-server:up       # Docker, localhost:6300
npm test                      # contract simulator suite + fixture invariants
npm run dev                   # http://localhost:5173

npm run compact is the gate that matters: a Compact contract that does not compile is an automatic disqualification for the wave. Run it before anything else.

Verified 2026-08-12 on compiler 0.31.1 (toolchain manager 0.5.1, x86_64 linux). A clean npm run compact produces contract/src/managed/alibi/{compiler/contract-info.json, contract/index.{js,d.ts,js.map}} with all four circuits present in the generated API.

On the version: earlier notes pinned 0.31.0. That was just the newest release when the event was scouted, not a deliberate choice — compact list now offers 0.31.1, and a judge following the steps above runs compact update and gets 0.31.1 too. Developing on the version the judge will actually run removes the drift the pin was meant to prevent. Same minor; the compiler is pinned in contract/compile.sh, which fails loudly on a mismatch.

For testnet: install Lace (Midnight Preview) on Chrome, request from the faucet, then click Generate tDUST — a separate second step that is easy to miss.

Scripts

Script Does
npm run compact Compile the Compact contract. The DQ gate.
npm test Contract simulator suite + UI tests + fixture invariants
npm run test:negative Just the negative suite — the tests that must fail
npm run verify Boundary + domain-separator + fixture checks
npm run verify:boundary Greps that /ledger and the proof path never touch mock data
npm run verify:fixtures Asserts the conservation identity across every fixture
npm run proof-server:up / :down / :logs Proof server on :6300
npm run dev / build / preview Vite
npm run lint ESLint, including the mock-data boundary rule

How judges can verify this is real

Not one of these depends on believing us.

  1. git clone && npm ci && npm run compact && npm run build && npm test on your own machine. The compiler runs, proving/verifying keys are generated, the suite passes including the negative cases.
  2. npm run test:negative — the in-mandate case throws an assertion failure. This product's core claim is a test that must fail.
  3. Kill the proof server mid-demo. The UI shows ERRORED_UNEXPECTEDLY naming the proof server — not REFUSED_AS_EXPECTED. A hardcoded refusal branch could not tell those apart. This is why the two states are built to be visually unmistakable: mint shield versus red octagon, in different regions of the screen, separable in greyscale and with audio muted.
  4. Query the public ledger yourself from your own indexer. The counters match what is on screen, and posted == executed + refused + outstanding holds.
  5. Type a different attack string than the scripted one — the commitment differs, the nullifier differs, the refusal still proves. Type the same one twice: the second is rejected.
  6. npm run verify:boundary — a dependency-free grep, readable in ten seconds, proving the /ledger route and the proof path never import mock data.

Testing

contract/src/test/ · vitest · an AlibiSimulator modelled on the Midnight example repos' CounterSimulator.

Every case asserts on both sides of the dual ledgergetLedger() and getPrivateState(). 7 positive cases, 12 negative, plus a 25-call randomized soak.

The negative suite is not a safety net; it is the specification. Every claim ALIBI makes is a claim that something cannot be done, and a claim of that form is worth exactly what its failing test is worth. Two matter most:

  • N1recordRefusal on an in-mandate instruction throws. You cannot fake a refusal you were not owed.
  • N5 / P5 — two refusals for two different reasons must produce byte-identical public state. If that ever fails, the disjunction is leaking and the product is broken.

Wave arc

Wave Delivers
1 Four circuits · public conservation identity · negative-test suite · judge-operated attack console
2 Contract custody of value (enforcement, not just attestation) · selective disclosure of reasonCode to the principal · fixed-depth Merkle counterparty allowlist · liveness/heartbeat layer
3 Delegation chains — principal → agent → sub-agent, where each hop can only narrow the mandate, proven in ZK

License

Apache License 2.0 — see LICENSE and NOTICE. All Midnight-related code in this repository is newly written for this Buildathon.


Design reference: DESIGN.md (technique) + docs/DESIGN_BRIEF.md (tone, palette, and the authority on both).

About

Prove your agent refused an order — without revealing the order, the leash, or where the leash sits. A privacy-first dApp on Midnight.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages