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
}
| Value | Number | Meaning |
|---|---|---|
None | 0 | No credential with this id exists in this set |
Active | 1 | Issued and in force (it can still be expired, see isValid on the Status registry) |
Suspended | 2 | Temporarily not in force; can return to Active |
Revoked | 3 | Cancelled for good. Terminal |
Superseded | 4 | Replaced 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;
}
| Field | Type | Meaning | Set by |
|---|---|---|---|
issuer | address | The issuer key that signed and sent the issuance. Zero means "no credential" | Issuer (checked equal to msg.sender) |
subjectCommitment | bytes32 | Salted 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 zero | Issuer's browser |
documentHash | bytes32 | SHA-256 of the document bytes. The verify page hashes a file locally and compares it with this value. Must not be zero | Issuer's browser |
metadataHash | bytes32 | Salted hash of the credential details (names, ids, title) kept in the holder's package. Must not be zero | Issuer's browser |
recordType | uint32 | Built-in id 0 to 31 or the issuer's custom id from 1000 (see Record type registry) | Issuer |
issuedAt | uint64 | Issue time in Unix seconds. May lie in the past; may be at most 5 minutes ahead of the block time | Issuer |
validUntil | uint64 | Expiry in Unix seconds, or 0 for no expiry. Must be later than issuedAt when set | Issuer |
schemaVersion | uint8 | Layout 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.
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;
}
| Field | Meaning |
|---|---|
status | Current value of the enum above |
reason | Reason 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 |
updatedAt | Block time of the last transition |
updatedBy | Account that made the last transition (for initialise, the issuer) |
supersededBy | Id of the replacement when status is Superseded, otherwise zero |
adminHold | True 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:
| Code | Label |
|---|---|
| 0 | Unspecified |
| 1 | Issued in error |
| 2 | Data correction |
| 3 | Fraud or misrepresentation |
| 4 | Academic misconduct |
| 5 | Administrative hold |
| 6 | Issuer key compromise |
| 7 | Holder 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:
| File | Pins |
|---|---|
blockchain/test/fixtures/credential-vector.json | A credential, its domain (Certichain, version 1, chain 31337) and the expected digest |
blockchain/test/fixtures/invitation-vector.json | An invitation and its digest |
blockchain/test/fixtures/commitment-vectors.json | Subject, 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.solblockchain/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