Skip to main content

Issuer key retirement

An institution signs credentials with one wallet address. Keys get lost, stolen or simply replaced. Key retirement (contract revision R7, contract version 0.5.0) lets the admin hand everything an old key issued to a successor key, so the older credentials stay valid and stay manageable.

Without it (on the 0.3.0 registries) a key change cannot keep older credentials manageable: the new address is a new issuer, and removing the old address takes everything it issued out of good standing.

The contract side is in Issuer registry. The admin path that runs retireIssuer is in Governance.

Who takes part​

ActorPart
InstitutionAsks for the change in writing, holds the new key, later manages the old credentials with it
Admin (the timelock, scheduled by the Safe)Registers the successor, then retires the old key in favour of it
GuardianFor a stolen key: suspends the old key at once while the retirement waits for the delay
VerifiersSee the change on the verify page; nothing else changes for them

Preconditions​

  • A contract set whose IssuerRegistry reports version 0.5.0 or later (it has retireIssuer, successorOf and currentIssuerOf). On Sepolia that is set 0.5.0, which is listed but not active as of 2026-10-24.
  • The successor is already a registered, active issuer with its own unique name (names stay with the old key; renaming is not offered).
  • The old key is registered, or is a removed key that others still point to.

The model​

Each retired key points to exactly one successor. currentIssuerOf(key) follows the pointers, at most 8 hops (MAX_LINEAGE_HOPS), and returns the key in charge.

FunctionMeaning
isIssuer(a)a has a registered name
isActiveIssuer(a)registered and not retired
successorOf(a)the key a was retired in favour of, or zero
currentIssuerOf(a)the end of the chain that starts at a

What the registries do with it:

ContractRule
CredentialRegistry.issueOnly an active issuer may issue (isActiveIssuer), so a retired key cannot issue
HolderRegistryInvitations from a retired key cannot be redeemed
RecordTypeRegistryA retired key cannot register types
StatusRegistrySuspend, reinstate, revoke and supersede are allowed for currentIssuerOf(credential issuer) only. A supersede's replacement must have the same key in charge.
StatusRegistry.isValidIssuer suspension is read for the key in charge, not the signing key

The credential itself never changes: its issuer field and the issuer's EIP-712 signature stay those of the old key. What changes is who may act on it and whose standing it follows.

Step by step​

The proposal command (from blockchain/, writes Safe batch files, sends nothing):

TARGET=IssuerRegistry FUNCTION=retireIssuer ARGS='["0x<old key>","0x<successor>"]' npm run timelock:propose:sepolia

The Safe owners compare both addresses with the institution's records before signing: a successor takes permanent control of the older credentials, and the only way out is to retire the successor onward.

Refusals​

ErrorWhen
NotIssuer(retired)The old key is neither registered nor pointed to by another key
KeyRetired(retired)The old key was already retired
InvalidSuccessor(successor)The successor is the old key itself, or not an active issuer
LineageTooLong()The chain would pass 8 keys
KeyRetired(issuer) from addIssuerSomeone tries to register a retired key again

A stolen key​

The order matters, because standing follows the successor once the retirement runs:

  1. The guardian suspends the old key at once (setIssuerSuspended(old, true, reason) on the StatusRegistry).
  2. List what the old key issued after the theft and revoke each fraudulent credential, either with the old key before the retirement or with the successor after it, or have the admin put a hold on it.
  3. If the old key had holder bindings or open invitations that could be abused, have it unbind and cancel them while it is still active. After the retirement nobody can change that key's holder bindings.
  4. Schedule the retirement. A retirement lifts the effect of the guardian's suspension of the old key (standing now comes from the successor), so anything not revoked by then is valid again.

If the successor itself is later compromised, the admin can retire it onward, even after removing it.

What users see​

WhereWhat
/verify?id=...The status stays as it was. A note says "The issuer key was retired in favour of 0x.... The credential was issued before the retirement and is judged by the key in charge now."
/verify Manage as issuerOpens only for the key in charge; the old key is denied
/dashboard with the old keyHeader Retired key and "This key was retired. 0x... is in charge of the credentials it issued, and this key can issue and change nothing more."
/dashboard with the successorLists what it issued and what the keys retired in favour of it issued (IssuedCredentialsService.issuedBy adds predecessorsOf)
/adminRetired issuer keys: each Retired key and the key it was retired In favour of
API, SDK, status lists, monitorJudge standing by the key in charge (lineageOf in src/lib/issuer-lineage.ts)

A set without lineage (0.3.0) has no currentIssuerOf; lineageOf treats the reverted call as "not retired", so the same code works on both kinds of set.

Limits​

  • Custom record types belong to the key that registered them. The successor registers its own, and a replacement of an older credential with a custom type uses one of the successor's types.
  • Encrypted documents are wrapped for the issuing wallet's key. The successor cannot open the old key's documents from the dashboard; re-issue them or keep the old wallet's document key.
  • Names stay with the old key.
  • After the retirement nobody can change the old key's holder bindings.
  • Offline checks (a package, a VC, a phone code) still show the old key as the signer. Only an online check sees the retirement.

Source files​

  • blockchain/contracts/v03/IssuerRegistry.sol (retireIssuer, currentIssuerOf, isActiveIssuer)
  • blockchain/contracts/v03/StatusRegistry.sol (_isIssuerOf, isValid), CredentialRegistry.sol, HolderRegistry.sol, RecordTypeRegistry.sol
  • src/lib/issuer-lineage.ts, src/services/domain/issuer-access-service.ts (retiredTo), src/services/domain/issued-credentials-service.ts
  • src/app/verify/IssuerRetiredNote.tsx, src/app/verify/ManageCredential.tsx, src/app/dashboard/IssuerDashboardPage.tsx, src/app/admin/RetiredKeysSection.tsx
  • blockchain/scripts/timelock-propose.ts
  • Handbook: docs/process/runbooks/timelock.md section 8, docs/onboarding/ (chapter on operating and leaving)

Tests​

  • Contract tests under blockchain/test/v03/ for the issuer registry lineage and the status registry rules (see Contract tests)
  • src/lib/issuer-lineage.test.ts
  • E2E: e2e/journeys/key-retirement.journey.ts (old credential stays active, the verify page names the successor, the old key's dashboard says it is retired, the successor revokes)