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
| Actor | Part |
|---|---|
| Institution | Asks 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 |
| Guardian | For a stolen key: suspends the old key at once while the retirement waits for the delay |
| Verifiers | See the change on the verify page; nothing else changes for them |
Preconditions
- A contract set whose
IssuerRegistryreports version 0.5.0 or later (it hasretireIssuer,successorOfandcurrentIssuerOf). On Sepolia that is set0.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.
| Function | Meaning |
|---|---|
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:
| Contract | Rule |
|---|---|
CredentialRegistry.issue | Only an active issuer may issue (isActiveIssuer), so a retired key cannot issue |
HolderRegistry | Invitations from a retired key cannot be redeemed |
RecordTypeRegistry | A retired key cannot register types |
StatusRegistry | Suspend, reinstate, revoke and supersede are allowed for currentIssuerOf(credential issuer) only. A supersede's replacement must have the same key in charge. |
StatusRegistry.isValid | Issuer 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
| Error | When |
|---|---|
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 addIssuer | Someone tries to register a retired key again |
A stolen key
The order matters, because standing follows the successor once the retirement runs:
- The guardian suspends the old key at once (
setIssuerSuspended(old, true, reason)on theStatusRegistry). - 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.
- 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.
- 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
| Where | What |
|---|---|
/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 issuer | Opens only for the key in charge; the old key is denied |
/dashboard with the old key | Header 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 successor | Lists what it issued and what the keys retired in favour of it issued (IssuedCredentialsService.issuedBy adds predecessorsOf) |
/admin | Retired issuer keys: each Retired key and the key it was retired In favour of |
| API, SDK, status lists, monitor | Judge 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.solsrc/lib/issuer-lineage.ts,src/services/domain/issuer-access-service.ts(retiredTo),src/services/domain/issued-credentials-service.tssrc/app/verify/IssuerRetiredNote.tsx,src/app/verify/ManageCredential.tsx,src/app/dashboard/IssuerDashboardPage.tsx,src/app/admin/RetiredKeysSection.tsxblockchain/scripts/timelock-propose.ts- Handbook:
docs/process/runbooks/timelock.mdsection 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)