A secure, privacy-preserving identity attribution system. An issuer attests a document (birth year, photo, attributes); the holder is enrolled in an indefinite-scale registry; and a verifier can confirm facts — "this is a genuine, enrolled identity, the holder is over 21, and this is the enrolled photo" — in zero knowledge, learning nothing else. Registry roots are anchored through the BSV-canonical Merkle tree(s) already in this repo set.
Ask "are you over 21?" and the holder answers with a zero-knowledge proof derived from the issued document — the birth year is committed, never revealed. The same pattern proves the photo binding and any number of other attributes. It generalises to any identity in a
2^256keyspace, not a hardcoded example.
| Crate | Role | Maps to |
|---|---|---|
idattr-smt |
Sparse Merkle tree over a 256-bit keyspace — inclusion and non-inclusion (revocation) proofs, lazy empty subtrees, O(256) per op | the indefinite-scale spine |
idattr-zkp |
Pedersen commitments + Schnorr-OR range proof (age ≥ N) + sigma opening (image/photo binding), Fiat–Shamir, on Ristretto |
SCARCITY 09 attestation spirit |
idattr-cred |
Issuer-signed document credentials (Ed25519), committed birth year + image hash, canonical CBOR, deterministic identity keys | issuance / enrolment |
idattr-anchor |
Federation: batch registry roots over epochs into vaa-merkle (WO 2022/100946 A1) and an independent BSV backend; two-hop proof to the on-chain anchor root |
"the merkle tree in git" |
idattr-cli |
idattr binary: demo, corpus, scale; the full issue→enrol→prove→verify flow |
E2E + generalization harness |
identity --SMT inclusion--> registry root --list-Merkle inclusion--> anchor root --on-chain
| | |
+-- Ed25519 issuer signature +-- ZK: age >= 21 (range proof) +-- one root, constant
over the credential ZK: photo == enrolled (opening) footprint at any scale
The verifier needs only the issuer key and the anchor/registry root. No secret ever reaches it.
- The registry is a full binary tree of depth 256 → 2^256 ≈ 1.16e77 possible identities.
- Empty subtrees are identical and precomputed, so storage tracks occupied leaves only.
- Every insert touches exactly 256 nodes; every proof is ≤256 siblings (only the non-empty ones sent).
- Identities are derived deterministically from a handle (
SHA-256), so any leaf is addressable without enumeration. Only one root is anchored on-chain regardless of population.
Measured (idattr scale): per-identity cost is flat as population grows —
population insert (us/op) prove+verify (ms/op)
1000 387.2 11.735
10000 426.7 12.366
50000 394.7 11.530
cargo test # 28 tests across the 5 crates
cargo run --release -p idattr-cli -- demo
cargo run --release -p idattr-cli -- corpus --count 10000
cargo run --release -p idattr-cli -- scale --steps 1000,10000,50000corpus --count 10000 (verified): issues + enrols 10,000 identities, then for every one builds a
holder bundle and verifies it — 8,177/8,177 adults proven over-21 and fully verified,
1,823/1,823 genuine minors unable to forge an over-21 proof (soundness), 0 anomalies, and a
never-enrolled "ghost" rejected by the non-inclusion check.
Three HTTP services + a one-shot acceptance client (idattr-svc):
| Service | Endpoints |
|---|---|
issuer (:8081) |
GET /key, POST /issue |
registry (:8082) |
POST /enrol, GET /root, POST /prove, GET /anchor |
verifier (:8083) |
POST /verify |
| client | one-shot: issue → enrol → prove → verify, asserts the result |
docker compose up --build --abort-on-container-exit --exit-code-from clientThe client drives the full flow across the services and exits non-zero unless attribution verifies;
it also runs a negative case (a never-enrolled "ghost" must fail the inclusion check). For a local
(non-container) run: idattr-svc issuer / registry / verifier in three shells, then
idattr-svc client.
On-chain anchoring is wired as documented compose comments: attach registry to the running Teranode
regtest network and provide its RPC. The broadcast itself needs a funded regtest key and is performed
by the scarcity-system/chain tooling — see HONESTY below.
idattr-anchor commits the epoch log of registry roots with every backend and checks they agree.
Today: the real vaa-merkle (patent WO 2022/100946 A1) and an independent BSV re-implementation
produce byte-identical anchor roots. The other trees in the repo set — the scarcity-system chain
tree, the triple-entry tee_merkle, the AnchorChain TS tree — plug in behind the same
MerkleBackend trait.
Real and tested in this workspace (28 tests + the 10k corpus): the SMT registry, the ZK age and image proofs, issuer signing, the full prove/verify flow over 10,000 identities, and the off-chain Merkle federation (registry root → anchor root) across two independent backends.
Needs live infrastructure or your credentials — intentionally NOT faked:
- On-chain anchoring of the anchor root — DONE (companion
idattr-onchain). The anchor root is broadcast via the note-anchoring template (SCARCITY REQ-CHAIN-0003, neverOP_RETURN): the 32-byte root rides as pushdata +OP_DROPahead of a native P2PKH spend tail, so the commitment output is itself the spendable possession outpoint (REQ-CHAIN-0001/0002, REQ-CHAIN-0051, REQ-BUILD-0010). Verified live against the Teranode regtest node (RPC127.0.0.1:9292) with a funded regtest key — accepted bysendrawtransactionand mined; seeidattr-onchain/ANCHOR_RECEIPT.md. - Device binding — interface + verifier DONE (
idattr-device); genuine hardware still needs real silicon.idattr-devicedefines the full attestation/binding interface: anAttestationover the app measurement, device key, freshness nonce and non-exportable flag (root-signed); a fail-closedverify_attestation(allowlisted measurement, fresh nonce, valid root sig); adevice_cert_commitment(device_pub, measurement)the issuer embeds in the credential (Issuer::issue_with_device); andbind/verify_bindingtying a presentation to the attested, enrolled device under a fresh challenge. The flow +idattr devicedemo show an honest presentation verifying while a replay (stale nonce) and a leaked credential presented from another device are rejected. Boundary kept honest:idattr-device'sSoftwareDeviceSeeis a software emulation — its "non-exportable" key and attestation root are ordinary in-process keys, not a secure element. Genuine non-exportability + a hardware-rooted attestation chain require thescarcity-device-client(Android StrongBox/TEE) on physical secure-element hardware, with the vendor attestation root verified off-device. The real quotes plug into this same verifier interface. A runnable software stand-in for that device lives in the companiontee-simrepo (a simulated TEE, CLI + HTTP), wire-compatible withidattr-deviceand pinned by a cross-repo known-answer vector. Seedocs/ARCHITECTURE.mdfor how all three boundaries fit together. - Compact bulletproofs backend — DONE (the bridge is wired). The in-repo range proof is
self-contained and linear-size (32
BitProofs); the AnchorChain TS bulletproofs give a logarithmic proof of the same age statement.anchorchain/packages/privacy/src/agebridge.ts(proveAgeAtLeastBP/verifyAgeAtLeastBP) expresses this crate's exact predicate on the secp256k1 Bulletproof: the verifier recomputesC_delta = threshold·G − C_birthfrom the issuer commitment, so soundness is identical toprove_age_at_least/verify_age_at_leasthere. The two backends are over different groups (Ristretto vs secp256k1), so the bridge federates the statement, not a shared commitment — to use it the issuer also commits the birth year in secp256k1. Verified: 6 tests on the same vector (birth 1990 / threshold 2005), proof is 5 inner-product rounds for 32 bits vs. 32 linearBitProofs here. - Document/biometric ingestion — pipeline + real MRZ validation DONE (
idattr-ingest); capture hardware, biometrics, and the issuer HSM remain real-world boundaries.idattr-ingestimplements a genuine ICAO 9303 TD3 / MRZ parser with the standard check-digit algorithm (tested against the Doc 9303 examples),DocumentCapture/PortraitCheckboundary traits, anIssuerRootstrust set, and anIngestPipelinethat validates the MRZ, runs the biometric check, derives(subject, birth_year, image_hash, attributes)and issues a committed credential. Theidattr ingestdemo runs simulated-scan → validate → issue → enrol → prove → verify; a MRZ with a bad check digit is rejected before issuance. Boundary kept honest: theSimulatedPassportScanneronly constructs a valid MRZ andAcceptingPortraitCheckdoes no matching — genuine capture (camera OCR / NFC chip read + passive authentication against the national CSCA), liveness + 1:1 face matching, and an HSM-held issuer key need real hardware/SDKs and the issuer's credentials.
-
idattr-smt— sparse Merkle registry (indefinite scale) -
idattr-zkp— age range proof + image binding (ZK) -
idattr-cred— signed document credentials + deterministic identity -
idattr-anchor— federated anchoring (vaa-merkle + independent backend) -
idattr-cli— E2E + 10k-corpus generalization + scale check -
idattr-svc+ Docker compose — issuer/registry/verifier services + acceptance client -
idattr-device— device-binding interface: attestation (measurement allowlist, fresh nonce, non-exportable, root-signed) + presentation binding to the issuer-certified device. Software SEE here; genuine hardware viascarcity-device-client.idattr devicedemo shows replay and leaked-credential rejection. -
idattr-ingest— issuer-side ingestion: real ICAO 9303 MRZ parse + check-digit validation, capture/biometric boundary traits, issuer-root trust, and an issue pipeline.idattr ingestdemo: simulated scan → validate → issue → enrol → prove → verify. Genuine capture/biometrics/HSM remain real-world boundaries. - Live on-chain broadcast of anchor roots — done (Teranode regtest), via the note-anchoring
template (no
OP_RETURN). See the companionidattr-onchaincrate and itsANCHOR_RECEIPT.md: a real registry root700e28d3…41d74d4committed in tx068093ae…97840580as a spendable<root> OP_DROP <P2PKH>possession outpoint, mined into block 309 (num_tx = 2) with a node-accepted BIP143/FORKID signature.