The zero-knowledge circuit at the heart of Corridor — a portable, private proof of eligibility for cross-border payments.
corridor_eligibility is a Noir circuit the holder runs on their own
device. It proves:
"I hold an unexpired credential of tier ≥ N, signed by the issuer identified by
issuer_id, whose credential epoch is at or above this corridor's revocation floor — and here is a per-corridor nullifier that can't be linked to my activity anywhere else."
…while revealing none of the holder's identity, tier, expiry, or the signature itself.
| Product & architecture | Sconce-Labs/corridor |
| Soroban contracts & the ABI | Sconce-Labs/corridor-contracts · ABI.md |
| Client SDK & the signer | Sconce-Labs/corridor-sdk |
| Constraint walkthrough | docs/CIRCUIT.md |
| Gate count & benchmarks | docs/BENCHMARKS.md |
| Revocation model | docs/REVOCATION.md |
There is no credential Merkle tree and no cross-chain accumulator. A regulated issuer runs KYC once, then signs a short-lived statement with a Grumpkin key (Barretenberg's embedded curve for BN254 — so signature verification is native in Noir, no non-native field arithmetic):
holder_binding = Poseidon2([holder_secret, salt]) // issuer never sees holder_secret
statement = Poseidon2([holder_binding, tier, expiry, cred_epoch])
(s, e) = schnorr_sign(issuer_sk, statement) // noir-lang/schnorr v0.4.0
The circuit verifies that signature plus every per-corridor predicate. The
earlier "Midnight builds a tree, a relayer syncs the root, the circuit proves
inclusion" design could not work — Midnight is BLS12-381, the circuit and
Stellar are BN254, so the roots are values in different fields. See
docs/CREDENTIAL_ACCUMULATOR.md.
Public inputs (9) — order is the ABI shared with corridor-contracts and
corridor-sdk:
| idx | name | type | idx | name | type | |
|---|---|---|---|---|---|---|
| 0 | corridor_id |
Field |
5 | issuer_id |
Field |
|
| 1 | min_tier |
u32 |
6 | min_cred_epoch |
u64 |
|
| 2 | now |
u64 |
7 | auditor_pubkey |
Field |
|
| 3 | nullifier |
Field |
8 | auditor_blob |
Field |
|
| 4 | disclosed_tag |
u32 |
Private witness: holder_secret, tier, expiry, cred_epoch, salt, issuer_pk_x, issuer_pk_y, sig_s_lo, sig_s_hi, sig_e_lo, sig_e_hi, auditor_nonce.
eligibility::check asserts:
holder_binding = Poseidon2([holder_secret, salt])statement = Poseidon2([holder_binding, tier, expiry, cred_epoch])schnorr::verify_signature(pk, (s, e), statement)— the issuer signed itPoseidon2([issuer_pk_x, issuer_pk_y]) == issuer_idtier >= min_tierexpiry > nowcred_epoch >= min_cred_epoch— bulk-revocation floornullifier == Poseidon2([holder_secret, corridor_id])— per corridor, unlinkabledisclosed_tag < MAX_TAG(16) — range only; the tag is a holder-chosen category label, not bound to any attribute (audit R2-H1)auditor_blob == Poseidon2([auditor_pubkey, tier, issuer_id, nullifier, auditor_nonce])
issuer_id ∈ accepted_issuers is checked on Soroban against the policy's
short allowlist, so the circuit stays fixed-size across corridors.
Cost: 73 ACIR opcodes (the Merkle-inclusion design was ~3200). See
docs/BENCHMARKS.md.
corridor-circuits/
├── corridor_eligibility/
│ ├── Nargo.toml deps: noir-lang/poseidon v0.3.0, noir-lang/schnorr v0.4.0
│ ├── Prover.toml a valid witness (generated — see FIXTURES.md)
│ ├── FIXTURES.md
│ └── src/
│ ├── main.nr the `pub` I/O adapter over eligibility::check
│ ├── eligibility.nr Public / Witness structs + check() + tests
│ ├── eligibility/fixture.nr good() — a witness with a real SDK Grumpkin signature
│ ├── tags.nr DisclosureTag constants (mirrors the SDK)
│ └── conformance.nr the pinned Poseidon2 vector
├── docs/ CIRCUIT.md · BENCHMARKS.md · REVOCATION.md
├── Makefile / justfile
└── .tool-versions nargo 1.0.0-beta.26
Prerequisite: noirup,
then:
noirup --version 1.0.0-beta.26
cd corridor_eligibility
nargo fmt --check
nargo check # type-check
nargo test # 18 tests — every failure mode
nargo execute # solve the committed Prover.toml witness
nargo info # gate count → 73 ACIR opcodes
# or:
make testProver.toml and src/eligibility/fixture.nr carry a real Grumpkin Schnorr
signature produced by corridor-sdk. Regenerate after any change to the
statement layout or the signing scheme:
cd ../corridor-sdk && npm run gen-fixture -- --writeNonces are derived deterministically (EdDSA-style), so an unchanged regeneration
is byte-identical. See corridor_eligibility/FIXTURES.md.
.github/workflows/ci.yml — every push and PR to
main:
| Job | Steps | Blocking |
|---|---|---|
check + fmt + test + execute |
install Noir 1.0.0-beta.26 · nargo fmt --check · nargo check · nargo test (18) · nargo execute on the committed fixture · nargo info (prints the gate count) |
✅ required for merge |
bb prove + verify |
install Barretenberg, bb prove / write_vk / verify (UltraHonk) on the fixture |
bb has no released version mapped to beta.26 yet, so a tooling break emits a ::warning rather than failing the build. A real proof failure still shows in the log. |
main is protected on the check + fmt + test + execute check.
Two hashes/schemes must agree bit-for-bit across three implementations, or nothing verifies:
| Circuit | SDK | Soroban | |
|---|---|---|---|
| Poseidon2 — arities 2 (nullifier, issuer_id, holder_binding), 4 (statement message) and 5 (auditor blob) each pinned to a fixed value | conformance.nr (noir-lang/poseidon) |
poseidon.test.ts (@zkpassport/poseidon2) |
poseidon_conformance (rs-soroban-poseidon) |
| Grumpkin Schnorr | noir-lang/schnorr v0.4.0 |
schnorr.ts — pinned to that library's test vector |
— (verified via the proof) |
CI's nargo execute on the SDK-signed fixture is the end-to-end check that the
SDK's signer and this circuit's verifier produce/accept the same signatures.
auditor_blobis a hiding commitment, not encryption — a warranted auditor can re-derive it over candidate values but cannot decrypt. In-circuit ECIES is milestone M7 (#3).bb prove/verifyin CI is best-effort until abbrelease maps to beta.26 (#1).- Targeted single-credential revocation (an on-Stellar indexed Merkle tree)
is designed in
docs/REVOCATION.mdbut deferred — shortexpiry+min_cred_epochcover the pilot.
Not audited — see SECURITY.md. A constraint bug means fake
passes accepted on Stellar; the eligibility.nr failure-mode tests are the
guard, and every new constraint gets one. holder_secret must be generated
with a CSPRNG (issuer/holder tooling — audit finding H5).
CONTRIBUTING.md. Issues labelled drips are
reward-eligible through the
Stellar Drips Wave
(DRIPS.md).
nargo test,nargo execute, andnargo fmt --checkmust pass.- A public-input layout change is a coordinated PR with
corridor-contracts(+ABI.md) andcorridor-sdk; regenerate the fixture. - Keep
conformance::PINNED{,_4,_5}green and the Schnorr scheme matchingschnorr.ts.
Apache-2.0 · see NOTICE.