Skip to content

oriontsx/certchain-program

Repository files navigation

CertChain — on-chain academic credential registry

CI License: MIT Anchor 0.31.1

Team TrustMint · Hack4FUTO 5.0

CertChain is a Solana / Anchor dApp for issuing and verifying academic credentials. An institution issues a credential by connecting its Solana wallet, filling a short form (student name, degree, department, year, grade, student wallet), and signing one transaction. The credential's canonical data is hashed (SHA-256) and the hash plus key fields are written on-chain; richer metadata lives off-chain (Supabase) in later sprints. A student views the credentials tied to their wallet, and a verifier confirms a credential by student wallet address or by QR (which resolves to the on-chain record), seeing institution / degree / year / grade, a Verified status, and the on-chain transaction link.

This repository is the Sprint 1 backend (blockchain) deliverable: the Anchor program (certchain), its on-chain data model, the duplicate-block guard, a full test suite + CI, CLI tooling (issue / query / hash), and a static web verifier.

  • Program ID: 4QFVyA8txKQM6rYsiDBJ4QrNurYtouaJq69KCfWXvKgV
  • Cluster: Devnet (https://api.devnet.solana.com)
  • Anchor: 0.31.1 · anchor-lang: 0.31.1

Contents


On-chain credential model

The Credential account (one PDA per credential):

Field Type Description
institution Pubkey Issuing institution wallet (the signer / payer of the issue tx).
student Pubkey Student wallet the credential belongs to.
student_name String Student full name (max 64 bytes).
degree String Degree / qualification (max 64 bytes).
department String Department / faculty (max 64 bytes).
year u16 Year of graduation.
grade String Grade / class of degree (max 32 bytes).
credential_hash [u8; 32] SHA-256 hash of the canonical credential payload — uniqueness key.
issued_at i64 Unix timestamp the credential was issued at (Clock).
bump u8 PDA bump.

Account size is computed at compile time via #[derive(InitSpace)] + #[max_len(..)], allocated as space = 8 + Credential::INIT_SPACE.

Errors

issue_credential validates field lengths before writing and reverts with a typed CertChainError:

Error Condition
StudentNameTooLong student_name exceeds 64 bytes
DegreeTooLong degree exceeds 64 bytes
DepartmentTooLong department exceeds 64 bytes
GradeTooLong grade exceeds 32 bytes

(A duplicate-hash submission reverts at init instead — see below.)


Why a hash-seeded PDA blocks duplicates

The credential is stored in a Program Derived Address seeded by the hash:

seeds = [b"credential", credential_hash]

Because the account address is a pure function of credential_hash, two identical credentials map to the same PDA. The issue_credential instruction uses Anchor's init, which fails if the account already exists. So a duplicate submission (same hash) reverts at the protocol level — no extra lookup, no race. This is the required edge case:

Duplicate credential submission → system detects the existing hash and blocks re-issuance.

On issue, the program also emits a CredentialIssued event (institution, student, credential_hash, issued_at).


How verification works

By hash (QR path). The verifier recomputes or reads the credential hash, derives the PDA with the same seeds, and fetches the account:

const [pda] = PublicKey.findProgramAddressSync(
  [Buffer.from("credential"), credentialHash], // credentialHash: 32-byte Buffer
  programId
);
const credential = await program.account.credential.fetch(pda);
// exists + fields match → Verified; the creating tx is the proof link.

By student wallet ("all credentials for a student"). Use getProgramAccounts / Anchor's account.credential.all([...]) with a memcmp filter on the student field. After the 8-byte discriminator the layout is institution (32) then student (32), so the student field is at offset 40:

const STUDENT_FIELD_OFFSET = 40; // 8 (discriminator) + 32 (institution)

const credentials = await program.account.credential.all([
  { memcmp: { offset: STUDENT_FIELD_OFFSET, bytes: studentPubkey.toBase58() } },
]);

The same constant is exported from the program as pub const STUDENT_FIELD_OFFSET: usize = 40;.

Issue / query / verify from the CLI

scripts/issue.ts and scripts/query.ts wrap the write + read paths as runnable helpers. They read the IDL from target/idl/certchain.json, so run anchor build first:

# (optional) precompute a credential's hash offline — no RPC needed
yarn hash --institution <pubkey> --student <pubkey> --name "Ada Lovelace" \
          --degree "B.Sc. CS" --department "Computer Science" --year 2026 --grade "First Class"

# issue a credential (the institution wallet signs + pays the rent)
yarn issue --student <pubkey> --name "Ada Lovelace" --degree "B.Sc. CS" \
           --department "Computer Science" --year 2026 --grade "First Class"
# ...or read the fields from a JSON file (CLI flags still override): yarn issue --from examples/credential.sample.json
# batch-issue from a JSON array (DRY RUN by default; add --send to issue): yarn batch-issue --from examples/credentials.batch.sample.json --institution <pubkey>

# verify ONE credential by its 32-byte SHA-256 hash (hex) — the QR path
yarn query verify  <credentialHashHex>

# list EVERY credential issued to a student wallet (memcmp at offset 40)
yarn query student <studentPubkey>

RPC defaults to devnet; set RPC_URL to target another cluster (e.g. RPC_URL=http://127.0.0.1:8899 for a local validator). Add --json to either query command for machine-readable output ({ verified, credential } for verify, { student, count, credentials } for student).

For a full end-to-end demo (build → issue → verify → list), see docs/walkthrough.md.

A zero-build static web verifier lives in verifier/index.html — serve it (npx http-server verifier, or any static host) and paste a credential hash to resolve the on-chain PDA and display the record (read-only, devnet by default). Opening it with a ?hash=<hash> query param (e.g. encoded in a QR) auto-verifies on load.


Flow

sequenceDiagram
    participant I as Institution
    participant P as CertChain program
    participant V as Verifier
    I->>P: issue_credential(hash, student, fields)
    Note over P: PDA = [b"credential", hash]<br/>init reverts if it already exists
    P-->>I: CredentialIssued event + tx signature
    V->>P: fetch PDA(hash)  /  memcmp by student (offset 40)
    P-->>V: Credential { institution, student, degree, ... }
    Note over V: exists + fields match → Verified
Loading

Build / deploy / test

Prerequisites: Rust + Cargo, the Solana CLI (Agave), and cargo-build-sbf (Solana platform-tools). Node + Yarn for the TS tests.

# 1. Point the Solana CLI at devnet (already the default in this workspace)
solana config set --url devnet

# 2. Install JS deps (test imports)
yarn install

# 3. Build + deploy + test with the Anchor CLI
anchor build
anchor deploy          # deploys to the cluster in Anchor.toml (devnet)
anchor test            # runs tests/certchain.ts against the program

Building without the Anchor CLI

The Anchor CLI is not required to compile the program. The on-chain program is verified to build with the Solana SBF toolchain directly:

# from the repo root (or programs/certchain)
cargo build-sbf

This produces the deployable target/deploy/certchain.so. (On this hackathon's dev box the Anchor CLI / AVM was not installed — the official Anchor NPM binary is x86_64-Linux only — so cargo build-sbf is the canonical build check here. anchor build / anchor test run unchanged on Linux/CI.)


Troubleshooting

anchor build fails: failed to parse manifest … requires the Cargo feature edition2024. The Solana platform-tools cargo bundled with older Agave (2.1.x) is too old to parse edition-2024 dependency manifests (e.g. crypto-common 0.2, constant_time_eq 0.4). Fix: pin a recent toolchain in Anchor.toml[toolchain] solana_version = "3.1.12" — so anchor build uses platform-tools v1.52 (cargo ≥ 1.85). This repo's CI already does that.

anchor test fails with … insufficient funds for spend. anchor test runs against [provider] cluster (which is devnet here), so it tries to deploy with an unfunded keypair. For local/CI runs, target a local validator: anchor test --provider.cluster localnet (Anchor starts one and funds the wallet).

The Anchor CLI won't install (Windows). The official Anchor NPM binary is x86_64-Linux only. On Windows, compile the program directly with cargo build-sbf (produces target/deploy/certchain.so); anchor build / anchor test run on Linux / CI.

CI uses Agave 3.x — stable-channel caveat. The stable Solana install channel now resolves to Agave 3.x, which needs io_uring; that works on ubuntu-latest (kernel 6.x) but can fail on older/sandboxed runners. CI pins v3.1.12 explicitly to stay reproducible.


Sprint scope

Sprint 1 (this deliverable) — DONE

  • Devnet configuration + program keypair / program ID.
  • certchain Anchor program: Credential account, issue_credential instruction, CredentialIssued event, custom error enum.
  • Duplicate-block guard via hash-seeded PDA + init.
  • On-chain write (issue) and read (fetch by PDA, list by student via memcmp) tests, the duplicate-revert assertion, field-length guard reverts, and a max-length boundary.
  • CLI toolkit (scripts/): hash (offline hash), issue (issue a credential, with --from <file>), query verify|student (read, with --json).
  • Zero-build static web verifier (verifier/index.html).
  • GitHub Actions CI (anchor build + anchor test on every push) + status badge.

Sprint 2 — deferred

  • Institution minting UI + full frontend wallet integration.
  • QR generation (issue) and QR scan → on-chain resolve (verify).
  • Off-chain rich metadata in Supabase.
  • Student / verifier dashboards wired to the program.

License

MIT

About

CertChain — Solana/Anchor program for on-chain academic credential verification (TrustMint, Hack4FUTO 5.0). Sprint 1 backend: credential storage smart contract.

Resources

License

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors