Skip to main content

Invitation file

An invitation file lets a student link a wallet to their record at one institution. The issuer signs an EIP-712 Invitation for one enrolment commitment and one wallet address. The student redeems it with that wallet on the HolderRegistry. The file names no student: it holds a salted commitment, not a name or student ID. The flow is described in Enrolment.

PropertyValue
File nameinvitation- followed by the first 8 hex characters of the invitation id, .json
Version1 (INVITATION_FILE_VERSION)
Made on/students/invite, section Invite a student, button Create invitation
Accepted on/student, section Accept an invitation, field Invitation file, button Accept invitation
Size accepted64 KiB
Produced bybuildInvitationFile in src/lib/invitation-file.ts
Read byparseInvitationFile, then verifyInvitationFile

Fields​

FieldTypeMeaningValidation
versionnumberFile versionMust be 1
domain.namestringEIP-712 domain name, "Certichain"Non-empty string
domain.versionstring"1"Non-empty string
domain.chainIdstringChain id, decimalDecimal
domain.verifyingContractstringAddress of the HolderRegistry of the active contract setAddress
invitation.issuerstringThe issuer wallet that signedAddress
invitation.enrolmentCommitmentstringThe student's enrolment commitment at this issuer32-byte hex
invitation.walletstringThe only wallet that may redeem itAddress
invitation.expirynumberUnix seconds after which it is deadSafe integer
invitation.epochnumberThe binding epoch the issuer read when signingSafe integer
invitation.noncestringRandom 256-bit nonce, decimalDecimal
signaturestringThe issuer's EIP-712 signature64 or 65 byte hex signature
invitationIdstringEIP-712 digest of domain and invitation, the on-chain id32-byte hex

The signed type, the same in TypeScript and Solidity:

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

How the values are chosen​

FieldSource
enrolmentCommitmentsubjectCommitment(issuer, studentId, studentSalt), where studentSalt = keccak256(signature) of a typed-data signature by the issuer wallet over StudentSalt(address issuer, string studentId) on the chain-free domain { name: "Certichain Student Salt", version: "1" }. The same student id always gives the same commitment for the same issuer key, so the issuer stores no salts.
expiryNow plus the lifetime chosen in Valid for: 1, 7, 14 or 29 days (default 7). The client allows at most 29 days so a signer clock that runs ahead stays inside the contract's 30-day limit.
epochHolderRegistry.epochOf(issuer, enrolmentCommitment) at signing time
nonce32 random bytes
domainThe chain id and the HolderRegistry address of the active set

The student salt is as secret as the issuer key, and it changes if the issuer key changes. A contract-wallet issuer cannot derive it ("This issuer account is a contract wallet, which cannot derive student identifiers yet. Use a plain wallet account to invite students.").

How it is checked​

The browser checks are a convenience; the contract checks all of them again, plus the epoch, the issuer's registration and the signature (EOA or ERC-1271). The issuer's app also confirms right after signing that the signature recovers to the issuer address, and refuses with "The wallet account changed while signing. Reconnect the university account and try again." if not.

CheckWhereMessage shown
invitationId does not match the fieldsBrowser"Invalid invitation file: invitationId does not match the fields"
Domain is another chain or registryBrowser"This invitation was issued for a different network or deployment."
Wallet is not the connected oneBrowser and contract (NotWallet)"This invitation was issued for a different wallet. Connect the wallet you gave your university."
ExpiredBrowser and contract (InvitationExpired)"This invitation has expired. Ask your university for a new one."
Expiry more than 30 days aheadContract (ExpiryTooFar)"The invitation expiry is too far in the future for the contract."
Issuer not activeContract (NotIssuer)"The university that issued this invitation is no longer registered."
Already usedContract (InvitationAlreadyRedeemed)"This invitation has already been used."
Epoch moved onContract (StaleInvitation)"This invitation was replaced or cancelled by your university. Ask for a new one."
Bad signatureContract (InvalidSignature)"The invitation signature is not valid."

Example​

Fake values.

{
"version": 1,
"domain": {
"name": "Certichain",
"version": "1",
"chainId": "11155111",
"verifyingContract": "0x3333333333333333333333333333333333333333"
},
"invitation": {
"issuer": "0x1111111111111111111111111111111111111111",
"enrolmentCommitment": "0x0000000000000000000000000000000000000000000000000000000000000a01",
"wallet": "0x4444444444444444444444444444444444444444",
"expiry": 1793404800,
"epoch": 0,
"nonce": "5521908734120938471209384712093847120938471209384712093847"
},
"signature": "0x<65-byte signature>",
"invitationId": "0x0000000000000000000000000000000000000000000000000000000000000f06"
}

Source files​

  • src/lib/invitation.ts: SignedInvitation, INVITATION_TYPES, invitationId, lifetime constants
  • src/lib/invitation-file.ts: buildInvitationFile, parseInvitationFile, verifyInvitationFile
  • src/lib/commitments.ts: deriveStudentSalt, subjectCommitment
  • src/services/domain/enrolment-service.ts: createInvitation, accept, error classes
  • src/lib/holder-errors.ts: revert messages
  • blockchain/contracts/v03/HolderRegistry.sol: invitationId, redeem, rebind

Tests​

  • src/lib/invitation.test.ts, src/lib/invitation-file.test.ts, src/lib/holder-errors.test.ts
  • src/services/domain/enrolment-service.test.ts
  • blockchain/test/v03/HolderRegistry.test.ts
  • E2E: e2e/journeys/invite-and-inbox.journey.ts, e2e/journeys/manage-student-link.journey.ts