Skip to main content

Status registry

A credential anchor never changes, but whether the credential still stands does. StatusRegistry records that second fact. It holds one StatusEntry per credential, moves it only through allowed transitions, keeps an issuer-wide suspension flag, and answers the question every verifier asks: isValid(id).

Nothing in this contract deletes anything. A revoked or superseded credential stays on the chain with its reason and history in events.

Inheritance, constructor and binding​

contract StatusRegistry is IStatusRegistry, AccessControlDefaultAdminRules {
uint16 private constant RESERVED_FROM = 8;
uint16 private constant ISSUER_DEFINED_FROM = 1000;
  • AccessControlDefaultAdminRules (OpenZeppelin 5) gives one default admin with a delayed two-step transfer, exactly as in the Issuer registry.
  • Constructor: (uint48 adminDelay, address admin, IIssuerRegistry issuers). A zero issuer registry reverts InvalidAddress().
  • After deployment the admin calls bindCredentialRegistry(registry) once. It reverts AlreadyBound() if already bound, InvalidAddress() for zero, and RegistryMismatch(registry) unless that credential registry's statusRegistry() is this contract. It emits CredentialRegistryBound(registry). The deploy script does this right after deploying the credential registry.

Storage in words​

ItemMeaning
credentialRegistryThe one credential registry allowed to call initialise and supersedeFor
_entries[id]The StatusEntry: status, reason, time, actor, replacement id, admin hold
_meta[id]The credential's issuer and validUntil, copied at initialise so isValid needs no call to the credential registry
_issuerSuspended[issuer]Issuer-wide suspension flag

The status machine​

  • Revoked and Superseded are terminal. No function leaves them.
  • "Issuer" always means the key in charge: issuerRegistry.currentIssuerOf(credential issuer). After a key retirement the successor holds this authority and the retired key loses it.
  • Status changes never check the pause flag. In an incident, revocation must keep working.

Who may do what​

This matrix is the one blockchain/test/v03/StatusRegistry.test.ts generates and checks case by case (6 starting states, 4 actions, 3 callers). "Suspended by admin" means a suspension with adminHold set.

Starting stateActionIssuerAdminAnyone else
None (unknown id)anyUnknownCredentialUnknownCredentialUnknownCredential
Activesuspendokok, sets admin holdNotAuthorised
ActivereinstateInvalidTransitionInvalidTransitionNotAuthorised
ActiverevokeokNotAuthorisedNotAuthorised
ActivesupersedeokNotAuthorisedNotAuthorised
SuspendedsuspendInvalidTransitionInvalidTransitionNotAuthorised
SuspendedreinstateokNotAuthorisedNotAuthorised
SuspendedrevokeokNotAuthorisedNotAuthorised
SuspendedsupersedeokNotAuthorisedNotAuthorised
Suspended by adminreinstateNotAuthorisedok, clears the holdNotAuthorised
Suspended by adminrevokeok, clears the holdNotAuthorisedNotAuthorised
Suspended by adminsupersedeInvalidTransitionNotAuthorisedNotAuthorised
Revoked or SupersededanyInvalidTransition (reinstate, revoke, supersede, suspend)InvalidTransition or NotAuthorisedNotAuthorised

The admin can only hold a credential. It cannot revoke, supersede or invent status for anyone's credential, so admin power over credentials is limited to a reversible freeze.

Functions​

State-changing​

FunctionWho may callChecksEffects and eventsReverts
initialise(bytes32 id, address issuer, uint64 validUntil)The bound credential registry onlyNot initialised beforeStores issuer and expiry; status Active; StatusChanged(id, None, Active, 0, issuer, 0)NotAuthorised, AlreadyInitialised
suspend(bytes32 id, uint16 reason, bytes32 noteHash)Key in charge, or adminKnown id; issuer calls need a valid reason; status is ActiveStatus Suspended; adminHold true when the admin (who is not also the issuer) suspends; StatusChangedUnknownCredential, NotAuthorised, InvalidReason, InvalidTransition
reinstate(bytes32 id, bytes32 noteHash)Key in charge, or admin when there is an admin holdKnown id; status is Suspended; a hold can only be lifted by the admin, an issuer suspension only by the issuerStatus Active, reason 0, hold cleared; StatusChangedUnknownCredential, NotAuthorised, InvalidTransition
revoke(bytes32 id, uint16 reason, bytes32 noteHash)Key in charge onlyKnown id; valid reason; status Active or SuspendedStatus Revoked, hold cleared; StatusChangedUnknownCredential, NotAuthorised, InvalidReason, InvalidTransition
supersede(bytes32 id, bytes32 replacementId, bytes32 noteHash)Key in charge onlyStatus Active or Suspended, no admin hold; replacement differs, has the same key in charge and is ActiveStatus Superseded, supersededBy set; StatusChanged and CredentialSuperseded(id, replacementId, this set's credential registry)UnknownCredential, NotAuthorised, InvalidTransition, ReplacementInvalid
supersedeFor(address issuer, bytes32 id, bytes32 replacementId, bytes32 noteHash)The bound credential registry only (from issueReplacing)issuer is the key in charge of id; then as supersedeAs supersede, with issuer as the actorNotAuthorised and as supersede
supersedeToRegistry(bytes32 id, bytes32 replacementId, address successorRegistry, bytes32 noteHash)Key in charge onlyStatus Active or Suspended, no admin hold; replacement not zero and not id; successorRegistry not zero and not this set's credential registryAs supersede, but the event names successorRegistry. The replacement is not checked on chain, because it lives in another setUnknownCredential, NotAuthorised, InvalidTransition, ReplacementInvalid, InvalidAddress
setIssuerSuspended(address issuer, bool suspended, uint16 reason)Suspend: guardian or admin. Lift: admin onlyRole check onlySets the flag; IssuerSuspensionChanged(issuer, suspended, reason, by)AccessControlUnauthorizedAccount
bindCredentialRegistry(address registry)AdminSee aboveCredentialRegistryBoundAlreadyBound, InvalidAddress, RegistryMismatch
Role and admin transfer functionsAdminOpenZeppelin rulesOpenZeppelin eventsOpenZeppelin errors

Views​

FunctionReturns
statusOf(bytes32 id) StatusEntryThe full entry (all zero for an unknown id)
isIssuerSuspended(address issuer) boolThe issuer-wide flag for that exact address
isValid(bytes32 id) boolThe good-standing answer, below
credentialRegistry() addressThe bound registry
contractVersion() string, supportsInterface(bytes4) bool"0.5.0" and ERC-165

Reason codes​

_requireIssuerReason refuses codes 8 to 999 in suspend (issuer calls) and revoke. Codes 0 to 7 are defined by the app (src/lib/status-reasons.ts, listed on Credential types), and 1000 and above are free for an issuer's own meanings. Admin suspensions skip the check. reinstate and supersede always record reason 0.

The noteHash argument is a hash of a free-text note the issuer may keep off chain. The app builds it in src/services/domain/note-hash.ts; the contract only stores it in the event.

isValid: good standing​

function isValid(bytes32 id) external view returns (bool) {
Meta storage meta = _meta[id];
if (_entries[id].status != CredentialTypes.Status.Active || _issuerSuspended[issuerRegistry.currentIssuerOf(meta.issuer)]) return false;
return meta.validUntil == 0 || block.timestamp <= meta.validUntil;
}

A credential is in good standing when all three hold:

  1. its status is Active;
  2. the key in charge of its issuer is not suspended;
  3. it has no expiry, or the block time is at or before validUntil (inclusive; ExpiryBoundary.test.ts pins valid at validUntil and invalid one second later).

Points to note:

  • Expiry does not change the stored status. An expired credential stays Active in statusOf while isValid returns false.
  • Issuer suspension follows the lineage. If the guardian suspends a key and the admin then retires it to a fresh successor, the old credentials return to good standing; if the successor is suspended, they leave it.
  • isValid does not check that the issuer is still registered. A removed issuer's credentials stay valid here. The verify page and the API check issuer registration separately and show it as its own result (see Verification).
  • A removed issuer that has no successor is still its own key in charge, so it can still suspend, revoke or supersede the credentials it issued.

Cross-set supersede​

When an issuer moves to a newer contract set, old credentials can point forward to their replacements:

The old set cannot read the new one, so a verifier must check the named replacement itself. The verify page does: it finds the replacement in any listed set and checks that it has the same key in charge and is in good standing. See Contract set upgrade.

Invariants the tests enforce​

  • Terminal states are never left; anchors never change (invariant suite, after every random action).
  • status, adminHold, supersededBy and isValid always equal a pure reference model (blockchain/test/invariants/model.ts).
  • No function deletes a credential or a status (StatusRegistry.test.ts, AC-6).
  • Only the bound credential registry can initialise and supersedeFor; binding happens once.
  • One set's status registry cannot change a credential of another set (TwoSets.test.ts).

How the app uses it​

  • src/services/domain/status-service.ts sends suspend, reinstate, revoke, supersede and supersedeToRegistry, and reads statusOf, isValid and isIssuerSuspended. src/services/domain/routed-status.ts sends a change to the set that holds the credential.
  • The Issuer tools section on the verify page (src/app/verify/ManageCredential.tsx, with a Reason list and a Note (only its hash is stored on-chain) field) and the issuer dashboard expose these actions to the issuer.
  • The status lists, the verification API, and the VC, badge, disclosure and presentation checks all read statusOf or isValid.

The user-facing flow is in Credential lifecycle.

Gas​

From blockchain/gas-snapshot.json: suspend 44,519; reinstate 44,682; revoke 42,265; supersede 76,397; supersedeToRegistry 69,555; admin hold (an admin suspend) 66,189; issuer suspension by a guardian 52,190.

Source files​

  • blockchain/contracts/v03/StatusRegistry.sol
  • blockchain/contracts/v03/interfaces/IStatusRegistry.sol
  • src/services/domain/status-service.ts, src/services/domain/routed-status.ts, src/services/domain/note-hash.ts, src/lib/status-reasons.ts

Tests​

  • blockchain/test/v03/StatusRegistry.test.ts: the full transition matrix, initialise and binding, reasons, supersede rules, issuer suspension, expiry, removed issuers, no delete function
  • blockchain/test/v03/Revisions.test.ts: supersedeFor and supersedeToRegistry (R1, R3)
  • blockchain/test/v03/KeyLineage.test.ts: authority and good standing after key retirement
  • blockchain/test/v03/ExpiryBoundary.test.ts: the inclusive expiry boundary
  • blockchain/test/v03/TwoSets.test.ts, blockchain/test/v03/Governance.test.ts
  • blockchain/test/invariants/Invariants.test.ts