Skip to main content

Key decisions

Every significant choice in Certichain is recorded as an architecture decision record (ADR) in docs/progress/DECISIONS.md in the repository. That log is long and chronological. This page groups the decisions that shaped the architecture by theme, gives the reason for each in a sentence or two, and names the ADR so you can read the full text, the options that were rejected and any later amendments.

How to read an ADR reference

"ADR-0088" means the entry headed ## ADR-0088 in docs/progress/DECISIONS.md. Later decisions sometimes refine earlier ones; where that happened, both are named here and the later one wins.

How the decisions build on each other​

Each phase rests on the one before it: the privacy layer needs the signed, commitment-based credential of Phase 1, the standards exports reuse its signature, and production readiness changes how the Phase 1 contracts are deployed and governed without changing what they mean.

Ground rules​

DecisionWhyADR
Sepolia (or a local Hardhat node) only until the production launch gate. No real names, student ids or documents until personal data is off chain and documents are encrypted; demo data must be obviously fictitious.A public chain never forgets. Testing with real people's data before the privacy work landed would have been irreversible.ADR-0007
Spec first and test-driven development for every step.Most of the system's guarantees are cryptographic and easy to break silently; a failing test written first pins the behaviour.ADR-0010
Prototype zero-knowledge code from early branches is never merged, and a mock verifier is never deployed.Those prototypes had mock verifiers and misaligned signals. A deployed mock verifier would accept any proof.ADR-0008

Contract design​

DecisionWhyADR
Build a new v0.3 contract set from scratch instead of patching the v0.2 AcademicRecords contract.The new anchor struct, event-first reads and namespaced identifiers would have meant a patchy rewrite, and there was no testnet data worth migrating.ADR-0034
OpenZeppelin 5 for v0.3 (v0.2 stays on 4.9).Custom errors, AccessControlDefaultAdminRules (a delayed, two-step admin transfer) and maintained EIP712, ECDSA and SignatureChecker.ADR-0035
Custom errors instead of revert strings.Smaller bytecode, lower gas, and ethers decodes them into typed errors the app can explain.ADR-0036
Issuer signatures checked with SignatureChecker, so EOAs and ERC-1271 contract wallets both work.Cheap at the start, and needed for a multisig issuer later; adding it afterwards would have changed the signing path.ADR-0037
recordType is a uint32: 0 to 31 are built in, 32 to 999 are reserved and refused, and 1000 and up are defined per issuer in a separate RecordTypeRegistry. (ADR-0038 first reserved the whole 0 to 999 range for built-in types; ADR-0053 narrowed it.)Institutions need their own record types without a contract change, and types must not collide between issuers.ADR-0038, ADR-0053
Addresses live in one generated manifest per chain; CredentialRegistry is the single entry point that exposes the other registries; every interface has a shared behaviour test suite.A redeploy or restructure costs only a regenerated manifest and a pass of the suites. No contract address is ever typed into an environment variable.ADR-0040
No unbounded loops: every v0.3 operation does constant work, guarded by source, ABI and gas tests; issuer names are capped at 128 bytes.A loop over a growing list eventually exceeds the block gas limit and locks the function for ever.ADR-0050
Contracts are immutable. A change ships as a new complete set next to the old ones; old sets stay live; one set is active for new issuance; every verification surface reads all listed sets.No upgrade key to steal, and a verifier can rely on the code behind an address never changing. The cost is routing by registry address everywhere.ADR-0022, ADR-0088
Revisions R1 to R5 (atomic replace-and-supersede, signature lookup without the stored block, successor registry hint, ERC-165 and version getters, a domain version rule) go into the first production set. R6 (batch anchoring) is built only if the chosen chain makes it worthwhile.Fix what a production deployment needs before it exists, and avoid building batching that a cheap chain would not need.ADR-0088, ADR-0089

See Contract versions and sets and Smart contracts.

Credentials, signatures and personal data​

DecisionWhyADR
The credential id is the EIP-712 digest of the signed Credential struct, under a domain that names the chain and the registry.The id commits to every signed field and cannot be replayed on another chain or registry. Clients compute it before issuing.ADR-0044
The issuer's EIP-712 signature is always required, even when the issuer sends the transaction itself, and is emitted in a separate CredentialSigned event. A per-issuer nonce is part of the typed data.The signature is the portable proof a holder carries away from the website. The extra event keeps CredentialIssued unchanged.ADR-0045
The status registry is bound once to its credential registry. Issuers manage their own credentials; the admin can only suspend, lift its own holds and suspend a whole issuer. An admin hold can be revoked by the issuer but not superseded.Nothing is ever deleted, and a stolen issuer key cannot quietly link a held credential to a replacement it controls.ADR-0046
No plaintext personal data on chain. The subject is a salted commitment and the display data is a salted metadata hash; the holder's package carries the display fields, the salt and the CID.An unsalted hash of a name and student id lets anyone confirm a guess against the chain.ADR-0047
Each student has one stable enrolment commitment per issuer, with a student salt derived from an issuer signature over typed data. Wallets bind only through issuer-signed, wallet-bound invitations (at most 30 days) with binding epochs that kill stale invitations.Lets a holder wallet see all its credentials from one issuer without putting the student id on chain, and lets a wrong or lost wallet be cancelled. Credentials of one student at one issuer become linkable on chain, never across issuers.ADR-0048, ADR-0049
Contract-wallet issuers can issue on chain, but the website cannot derive student salts or encryption keys for them.Their signatures can change with the signer set, so anything derived from a signature would not be repeatable.ADR-0049, ADR-0063

See Issuing a credential, Enrolment and Privacy design.

Reading the chain​

DecisionWhyADR
No hosted indexer. Lists are built from topic-filtered eth_getLogs scans in bounded, resumable windows from the manifest's deployment block, behind a CredentialIndex interface, with a browser cache.Nothing new to host or secure. A hosted indexer can replace the implementation later without changing pages or services.ADR-0051, ADR-0052
issueBlock(credentialId) lets a client fetch the signature event with a one-block query. Later revised so the lookup works by event topic too (R2).Public RPC providers cap eth_getLogs ranges, and on some chains the stored block number is not the one the RPC reports.ADR-0045, ADR-0088
One RPC provider factory with ordered failover at every call site, and retries with backoff on log scans.A single flaky public endpoint should not break verification.ADR-0087

See Reading the chain.

Server​

DecisionWhyADR
Uploads need a Sign-In with Ethereum session in a short-lived HttpOnly cookie, built on siwe and jose; rate limits and nonces live in Upstash Redis in production and in memory locally.Only registered issuers may pin files, without accounts or passwords.ADR-0016, ADR-0026
The upload request cap is 4 MB, so a document may be at most 3.5 MB and its encrypted, padded container still fits.Vercel functions reject bodies over 4.5 MB before the route runs.ADR-0029, ADR-0065
A role-check outage answers 503, never "allowed"; rate limits are atomic; client IP headers are trusted only on Vercel.Fail closed, and stop a caller from spoofing its address to dodge limits.ADR-0031
Only issuers pin. Holders who want a recipient cut off from future copies ask the issuer to rotate the key.Keeps the upload route's single rule and its abuse surface small.ADR-0063
Verification API answers are EIP-712 signed by a dedicated, funds-free key with an echoed nonce and a signed data age; API keys are stored only as SHA-256 hashes.A client that pins the signer address gets integrity beyond the transport, and a cached answer cannot pass for a fresh one.ADR-0083
Status lists are signed by a separate funds-free publisher key and served from the deployment's own domain.A key compromise affects availability of lists only; verifiers that read the chain are unaffected.ADR-0078

See Server and APIs.

Documents and sharing​

DecisionWhyADR
Documents are encrypted in the browser before upload (AES-GCM content key, wrapped to recipients with X25519 through HPKE). The issuer's X25519 key is derived from a repeatable wallet signature over EncryptionKey(owner, keyVersion), held in memory only. The app signs twice and refuses a wallet whose signatures differ.No key registry and no contract change; the server and the pinning service only ever see ciphertext. Accepted weakness: any site that obtains the same signature derives the same key.ADR-0063, ADR-0065
The document key travels in the credential package, which becomes the secret that opens the document.A holder can open the document with the package alone, without a wallet.ADR-0063
Sharing uses share codes (a checksummed public key with a fingerprint compared out of band), grant files and capability links whose key sits in the URL fragment. A grant discloses the signed fields, the CID and the wrapped key only. Key replacement removes the issuer's older pins, never the last one.Narrower than handing over the whole package. Replacement is best-effort clean-up, not revocation: someone who saved the old container and key keeps access.ADR-0066, ADR-0067

See Document storage and Sharing.

Zero-knowledge access and selective disclosure​

DecisionWhyADR
Private access uses Semaphore v4 unchanged (circuit, Groth16 verifier, group contract) behind a thin CertichainAccessGate. The bound holder wallet or the issuer admits a requester; a proof earns an on-chain receipt only, and documents are still released off chain through grants.Reuse an audited, widely used circuit instead of writing one, and keep the proof's power small.ADR-0068
Circuit artifacts are pinned and checked: the verification keys of all pinned zkeys must match the pinned verifier, and the zkeys reproduce from the circuit source against the public Powers of Tau.Shows consistency between what is deployed and what was published. It cannot show that any ceremony contributor was honest.ADR-0069, ADR-0070
The browser prover ships, with GPL-3.0 snarkjs imported only inside the proving worker.Proofs in the browser in one to two seconds. The licence boundary stays one file wide; the repository licence text is the owner's to write.ADR-0072
Selective disclosure needs no circuit: the issuer signs a tree of salted display fields, the holder signs a disclosure file bound to the verifier's challenge, and the verify page checks it without a wallet.Disclosing chosen fields is enough for the current use; a predicate proof waits until an institution asks for one.ADR-0074, ADR-0075

See Private access proofs and Selective disclosure.

Standards and integrations​

DecisionWhyADR
The credential package stays canonical; a W3C VC 2.0 document is an export of it, never containing the document key or the CID. Issuers are did:pkh:eip155:<chainId>:<address>. The proof reuses the issuer's EIP-712 signature as the certichain-eip712-2026 cryptosuite.One source of truth and no second issuer key for the basic export. Third-party verifiers need a small plugin, which the interop tests prove works.ADR-0076
Open Badges 3.0 is a separate export. A second, standard eddsa-rdfc-2022 proof is added by the issuer from a key derived from and attested by the wallet.Badge platforms expect a standard suite that verifies with no plugin.ADR-0079, ADR-0082
One independent JavaScript verifier library checks the exports in CI.Proves the formats are readable by code Certichain did not write.ADR-0080
A private, unpublished SDK accepts API answers only from a pinned signer, with no signer discovery.Integrators get the API's integrity guarantees without writing signature code.ADR-0084
The phone wallet is an installable web app with compact static QR codes checked offline, plus a holder-bound presentation signed for the verifier's challenge. Only hashes, addresses and the signature are stored by default.No app store and no relay service. A static code is a copyable bearer proof, and the pages say so.ADR-0085

See Integrations and Data formats.

Governance​

DecisionWhyADR
The IssuerRegistry holds a guardian role and the pause flag. Pause stops creating things (issue, redeem, rebind, register a type) and never stops revocation, suspension or reads. The guardian can pause and suspend an issuer at once; the admin transfer is delayed and two-step (3 days on Sepolia).A fast brake that cannot be used to undo history, and an admin change that everyone can see coming.ADR-0057
The admin of the v0.3 registries on Sepolia is a 2 of 3 Safe.No single key controls issuer registration.ADR-0058, ADR-0060
An OpenZeppelin timelock run by the Safe: the Safe proposes and cancels, anyone executes after the delay (2 days in production, 1 hour for the Sepolia rehearsal).A public delay before any admin action runs, so issuers and verifiers can react.ADR-0090, ADR-0092
Two revisions in a new set: R7, retiring an issuer key in favour of a successor so older credentials stay in good standing; R8, the guardian may unpause.Institutions rotate keys, and recovery from a pause should not wait for the admin delay.ADR-0090, ADR-0091
Consortium admission is a charter and a recorded accreditor sign-off whose hash becomes the timelock salt, not a contract rule.Keeps the contracts unchanged while giving a public link between an admission and its approval.ADR-0095

See Governance and Key retirement.

Operations and the interface​

DecisionWhyADR
Alerts go to one configured webhook; a scheduled route runs a read-only monitor; a second pinning provider sits behind a PinProvider interface; the RPC list holds two hosted providers and the chain's public endpoint.Vendor-neutral code, configured by lists. Everything stays off until the owner sets the secrets.ADR-0087
Service levels are measured by the same monitor with daily counts and a canary probe, and reported monthly.No public status page to run, but numbers partners can hold the service to.ADR-0095
The production chain is chosen on measured gas per operation, a cost model and a compatibility checklist; security and long-term checkability come before cost.A credential must stay checkable for decades, which outweighs a cheaper fee.ADR-0086
The interface follows one "modern security print" design system: OKLCH tokens, shared primitives, role workspaces, and a deterministic seal per credential that is a recognition aid only.One coherent product, and a visual cue that never pretends to be a check.ADR-0096

See Operations and Web application.

Source files​

  • docs/progress/DECISIONS.md (the full decision log)
  • docs/progress/DEVIATIONS.md (where the build departed from a plan, and why)
  • docs/specs/ (the specs each ADR confirms)