Skip to main content

Contract set upgrade

Certichain contracts are not upgradeable proxies. A change to the contracts means a new contract set: a complete, independent group of registries (IssuerRegistry, CredentialRegistry, StatusRegistry, HolderRegistry, RecordTypeRegistry) deployed next to the existing ones. Nothing is changed in place and nothing is copied between sets (ADR-0088).

Every set stays listed in the deployment manifest for ever. One set is active: new credentials are issued there. Every credential is checked in the set whose CredentialRegistry signed it, which the credential itself names (its EIP-712 domain's verifyingContract).

The idea is explained in Contract versions and sets. How the frontend picks a set per credential is in Contract sets in the frontend. The manifest file is described in Deployment manifest.

Why sets instead of upgrades​

  • A verifier who pinned a registry address keeps getting the same answers. An old credential is never re-interpreted by new code.
  • The signature domain binds each credential to one registry, so a proxy upgrade would change what a signature means. A new set has a new domain instead.
  • Rolling back is a manifest edit: set activeSet to the previous label and rebuild. No chain state is lost.

The cost is that holders move forward only by a new issuance, and several sets must be watched and governed.

Who takes part​

ActorPart
Developer or operatorDeploys the set, records it in the manifest, runs the readiness check, activates it
Safe ownersAccept the admin role for the new registries (or schedule it through the timelock), schedule addIssuer for institutions carried over
IssuersKeep issuing; after activation their new credentials go to the new set; optionally re-issue older credentials
VerifiersNothing changes for them, apart from pinning the extra registry address in API and SDK clients

Lifecycle of a set​

A listed set (active or not) is read by every verification surface: the verify page, the API, the SDK, status lists, presentations and the monitor. Only the active set receives new credentials.

Step 1: deploy and record the set​

From blockchain/, first on a local chain:

npm run deploy:v03:local # the first set
ADD_SET=1 SET_LABEL=next ISSUER_REGISTRY=<shared issuer registry> ACTIVATE_SET=1 npm run deploy:v03:local
VariableMeaning
ADD_SET=1Add the set beside the others. Without it a deployment writes a manifest with only this set, and never replaces the manifest of a public chain.
SET_LABEL1 to 32 letters, digits, dots, dashes or underscores; unique in the manifest
ISSUER_REGISTRYShare an existing issuer registry (then a pause or an issuer change applies to both sets). Must answer the guardian role and the version. A set from contract version 0.5.0 on needs a registry with key lineage, so it cannot share an older 0.3.0 registry.
ACTIVATE_SET=1Make the new set active immediately. Leave it off to switch later.
ADMIN_DELAY_SECAdmin transfer delay of the new registries (default 3 days)
GUARDIAN_ADDRESSGuardian of a new issuer registry (ignored with a shared one)

All manifest checks (label, network, file readable) run before the first transaction. On Sepolia the same script runs as deploy:v03:sepolia, which needs the human's go-ahead for each deployment. Then the contracts are verified on Etherscan (SET=<label> npm run verify:v03:sepolia).

Step 2: put the set under governance​

A new set starts with its deployer as admin of its issuer and status registries. Before activation it must be administered like the others:

  • before the timelock era, nominate the Safe (handover:v03:sepolia) and accept from the Safe;
  • with a timelock, the deployer nominates it (SET=<label> npm run timelock:nominate:sepolia) and the Safe schedules the accept (SET=<label> npm run timelock:handover:sepolia).

See Governance.

Step 3: carry issuers over​

A set with its own issuer registry starts empty. Each institution that issues in the active set needs addIssuer in the new registry, scheduled through the timelock. Names only need to be unique inside one registry, so an institution can keep its name. Retired keys and removed institutions are not carried over.

Step 4: readiness and activation​

SET=<label> npm run set:ready:sepolia # read only
SET=<label> npm run set:activate:sepolia # rewrites the manifest, sends nothing

checkSetReadiness blocks activation on:

  • a contract with no code;
  • a paused set;
  • registries not administered by the recorded timelock;
  • a status registry not bound to its credential registry;
  • any issuer of the active set that cannot issue in the new set.

set:activate:sepolia runs the same check, then rewrites activeSet in src/contracts/v03/deployment.<chainId>.json (through withActiveSet). The manifest is committed and the site rebuilt; the runbook asks for a preview deployment first, with a carried-over issuer issuing, verifying, revoking and opening a document.

Step 5: moving holders forward​

A holder moves to a new set only through a new issuance. From the dashboard the issuer re-issues a credential (see Lifecycle):

  • Same set: the new credential is issued and the old one superseded atomically.
  • Across sets: the new credential is issued in the active set, then a second transaction calls supersedeToRegistry(id, replacementId, successorRegistry, noteHash) on the old set's StatusRegistry. If that second step is rejected, the dashboard shows a reminder that finishes it.

A registry cannot read another set, so the cross-set link is not checked on chain. The verify page checks it on the reader's side: it looks for the replacement in every listed set, requires the same key in charge, reads its standing, and warns when it is missing or belongs to another issuer.

How each surface finds the right set​

SurfaceRouting
Verify page (lookup by id)CredentialLocator asks the active set first, then the older ones, and shows the one that answered; an older set adds "This credential was issued in an older contract set. It is checked in that set and stays there." (Contract set: label)
Package, VC, badge, disclosure filesThe file's verifyingContract picks the set
Phone codesPresentationRouter picks by the code's registry; an unlisted registry goes to the active set, which reports the mismatch
Dashboard and holder inboxList credentials from every set; revoke, open document, key replacement, disclosure and export go to the set that holds the credential
Status listsStatusListRouter serves lists per registry
Verification API and SDKThe API lists the sets in GET /api/v1/status and refuses a file that names an unlisted registry; SDK clients pin a list of registries
MonitorWatches events and admins of every listed set

The VersionAnnouncer​

blockchain/contracts/v03/VersionAnnouncer.sol is an append-only notice board: announce(label, abiVersion, addresses) records where a set lives, checks the five addresses belong together, and emits VersionAnnounced. Entries cannot be changed or removed. It is advisory only: nothing in the app reads it, and verifiers take addresses from their own configuration. It is not listed in the manifest.

Sepolia as of 2026-10-24​

SetDeployedStatus
0.3.02026-10-13, block 11904844Active. Admin of its registries: the Safe
0.5.02026-10-17, block 11932876Listed, not active. Own issuer registry with key lineage, under the timelock. The readiness check blocks activation because the three test issuers of 0.3.0 are not registered in it.

Addresses are in Deployments.

Limits​

  • A cross-set replacement is a claim by the issuer that only readers check.
  • The ZK access gate serves one credential registry (the 0.3.0 one on Sepolia). Another set needs its own gate.
  • A phone with no network gives the signature-only verdict for every set.
  • Retiring a set is a separate recorded decision; until then it stays listed, watched and governed.

Source files​

  • src/contracts/v03/manifest.ts (parseManifestSets, findSetByRegistry, addSet, withActiveSet, withTimelock)
  • blockchain/scripts/deploy-v03.ts, blockchain/scripts/lib/deploy-v03.ts, blockchain/scripts/lib/record-deployment.ts
  • blockchain/scripts/check-set-ready.ts, blockchain/scripts/activate-set.ts
  • blockchain/contracts/v03/VersionAnnouncer.sol, blockchain/contracts/v03/StatusRegistry.sol (supersedeToRegistry)
  • src/services/domain/routed-status.ts, src/services/domain/presentation-router.ts, src/lib/server/status-lists/router.ts
  • src/app/verify/SetNote.tsx
  • Handbook: docs/process/runbooks/contract-sets.md

Tests​

  • src/contracts/v03/manifest.test.ts
  • src/services/domain/routed-status.test.ts, src/lib/server/status-lists/router.test.ts
  • Contract and script tests under blockchain/test/ (see Contract tests)
  • E2E: e2e/journeys/contract-sets.journey.ts (the E2E harness deploys two live sets on its local chain)