Skip to main content

Credential types

CredentialTypes is a Solidity library with no functions. It declares the data shapes every v0.3 registry shares, so the credential registry, the status registry, the access gate and the TypeScript code agree on one layout. This page explains each field: what it holds, where it comes from and why it is there.

How the shapes relate​

The issuer signs a Credential; the credential registry stores the same fields (minus the nonce, plus schemaVersion) as a CredentialAnchor under the id; the status registry keeps one StatusEntry per id.

The status enum​

enum Status {
None,
Active,
Suspended,
Revoked,
Superseded
}
ValueNumberMeaning
None0No credential with this id exists in this set
Active1Issued and in force (it can still be expired, see isValid on the Status registry)
Suspended2Temporarily not in force; can return to Active
Revoked3Cancelled for good. Terminal
Superseded4Replaced by another credential, named in supersededBy. Terminal

The numbers matter because events and statusOf return the enum as a uint8. The app maps them back to names in its status service.

CredentialAnchor: what is stored per credential​

struct CredentialAnchor {
address issuer;
bytes32 subjectCommitment;
bytes32 documentHash;
bytes32 metadataHash;
uint32 recordType;
uint64 issuedAt;
uint64 validUntil;
uint8 schemaVersion;
}
FieldTypeMeaningSet by
issueraddressThe issuer key that signed and sent the issuance. Zero means "no credential"Issuer (checked equal to msg.sender)
subjectCommitmentbytes32Salted commitment to the student within the issuer's namespace. It is the same value as the enrolment commitment the Holder registry binds a wallet to, so the chain can link a credential to a holder wallet without naming anyone. Must not be zeroIssuer's browser
documentHashbytes32SHA-256 of the document bytes. The verify page hashes a file locally and compares it with this value. Must not be zeroIssuer's browser
metadataHashbytes32Salted hash of the credential details (names, ids, title) kept in the holder's package. Must not be zeroIssuer's browser
recordTypeuint32Built-in id 0 to 31 or the issuer's custom id from 1000 (see Record type registry)Issuer
issuedAtuint64Issue time in Unix seconds. May lie in the past; may be at most 5 minutes ahead of the block timeIssuer
validUntiluint64Expiry in Unix seconds, or 0 for no expiry. Must be later than issuedAt when setIssuer
schemaVersionuint8Layout version of the anchor, written by the contract (SCHEMA_VERSION = 1)Contract

How the commitments and hashes are built is explained in Hashing and commitments. The values a holder needs to recompute them travel in the Credential package.

No block number in the anchor

Sets labelled 0.4.0 and later store no block number for a credential. The contract set 0.3.0 kept an extra issueBlock mapping; revision R2 removed it, and clients now find the CredentialSigned event by its indexed topics instead.

Credential: what the issuer signs​

/// @dev What the issuer signs (EIP-712). `credentialId` is the digest of this struct.
struct Credential {
address issuer;
bytes32 subjectCommitment;
bytes32 documentHash;
bytes32 metadataHash;
uint32 recordType;
uint64 issuedAt;
uint64 validUntil;
uint256 nonce;
}

The signed struct is the anchor without schemaVersion and with a nonce. The nonce is chosen by the issuer and may be used once per issuer; the credential registry records used nonces in isNonceUsed(issuer, nonce).

The EIP-712 type string, copied from CredentialRegistry.sol:

Credential(address issuer,bytes32 subjectCommitment,bytes32 documentHash,bytes32 metadataHash,uint32 recordType,uint64 issuedAt,uint64 validUntil,uint256 nonce)

The domain is EIP712("Certichain", "1") with the chain id and the credential registry address that OpenZeppelin's EIP712 adds. The credential id is the full EIP-712 digest of this struct (credentialId(credential)), so:

  • the id is bound to one chain and one credential registry, and the same content issued in another set gets a different id;
  • the issuer's signature is a signature over the id itself, so checking the signature and checking the id are the same computation;
  • anyone holding the package can recompute the id offline.

See Signatures for EIP-712 in general.

StatusEntry: the status of one credential​

struct StatusEntry {
Status status;
uint16 reason;
uint64 updatedAt;
address updatedBy;
bytes32 supersededBy;
bool adminHold;
}
FieldMeaning
statusCurrent value of the enum above
reasonReason code of the last transition (0 when none). Codes 8 to 999 are reserved and refused in issuer calls; 1000 and above are issuer-defined
updatedAtBlock time of the last transition
updatedByAccount that made the last transition (for initialise, the issuer)
supersededById of the replacement when status is Superseded, otherwise zero
adminHoldTrue when the admin, not the issuer, suspended the credential. Only the admin can lift it

The app's labels for reason codes 0 to 7 live in src/lib/status-reasons.ts:

CodeLabel
0Unspecified
1Issued in error
2Data correction
3Fraud or misrepresentation
4Academic misconduct
5Administrative hold
6Issuer key compromise
7Holder request

The contract does not interpret these codes. It only refuses the reserved range in issuer calls.

Invitation (holder registry)​

The Holder registry declares its own signed struct in IHolderRegistry:

Invitation(address issuer,bytes32 enrolmentCommitment,address wallet,uint64 expiry,uint64 epoch,uint256 nonce)

It uses the same EIP712("Certichain", "1") domain name and version, but the verifying contract is the holder registry, so an invitation signature can never be replayed as a credential signature. The file that carries it is described in Invitation file.

Shared test vectors​

TypeScript and Solidity compute the same digests. Fixed vectors pin this:

FilePins
blockchain/test/fixtures/credential-vector.jsonA credential, its domain (Certichain, version 1, chain 31337) and the expected digest
blockchain/test/fixtures/invitation-vector.jsonAn invitation and its digest
blockchain/test/fixtures/commitment-vectors.jsonSubject, enrolment and metadata commitments

They are read by the contract tests and by src/lib/eip712.test.ts, src/lib/commitments.test.ts, src/lib/invitation.test.ts and src/lib/invitation-file.test.ts, so a change on either side fails a test.

Source files​

  • blockchain/contracts/v03/libraries/CredentialTypes.sol
  • blockchain/contracts/v03/CredentialRegistry.sol (type string, domain, credentialId)
  • blockchain/contracts/v03/interfaces/IHolderRegistry.sol (Invitation)
  • src/lib/eip712.ts, src/lib/commitments.ts, src/lib/status-reasons.ts

Tests​

  • blockchain/test/v03/CredentialIssuance.test.ts (digest equals the shared vector, field checks)
  • blockchain/test/v03/HolderRegistry.test.ts (invitation digest)
  • blockchain/test/v03/NoPlaintext.test.ts (issues a credential and checks that the student's name, id, wallet and the document CID appear in no transaction log and no storage slot of any v0.3 contract; also reproduces the commitment vectors with plain ethers)
  • src/lib/eip712.test.ts, src/lib/commitments.test.ts