|
1 | | -# Sample Hardhat Project |
| 1 | +# On-Chain Layer — SCAR / DiGo Certify |
2 | 2 |
|
3 | | -This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a Hardhat Ignition module that deploys that contract. |
| 3 | +This is the [Hardhat](https://hardhat.org/) project for the **on-chain layer** of |
| 4 | +SCAR / DiGo Certify: an academic-certificate registry built by repurposing |
| 5 | +**OnchainID** (ERC-734/735 self-sovereign identity) and **T-REX / ERC-3643** |
| 6 | +(permissioned-token compliance) into a registry for identity-bound, accredited, |
| 7 | +updatable, and privately verifiable credentials. Verification runs directly |
| 8 | +against the ledger, so it does not depend on the client application. |
4 | 9 |
|
5 | | -Try running some of the following tasks: |
| 10 | +Contracts are compiled with Solidity **0.8.17 / 0.8.20**, optimizer **enabled**, |
| 11 | +**`runs = 200`** (see `hardhat.config.js`). |
6 | 12 |
|
7 | | -```shell |
8 | | -npx hardhat help |
| 13 | +## Project structure |
| 14 | + |
| 15 | +| Path | Contents | |
| 16 | +|------|----------| |
| 17 | +| `contracts/` | Solidity sources — the reused OnchainID / T-REX suite plus this project's additions (`contracts/config/FileStorage.sol`, `contracts/config/HashAnchor.sol`). | |
| 18 | +| `scripts/` | The certificate life-cycle operations (deploy identities/issuers, add keys, add/read claims, encryption helpers) used by the app and the benchmarks. | |
| 19 | +| `test/` | Hardhat/Mocha tests, including the negative/security tests in `test/security/` and the shared `test/fixtures.js`. | |
| 20 | +| `eval/` | Benchmarking scripts (`measure.js`, `baseline.js`, `sweeps.js`, `latency.js`), their CSV output under `eval/results/`, and the committed Slither reports. | |
| 21 | + |
| 22 | +## Getting started |
| 23 | + |
| 24 | +```sh |
| 25 | +npm install |
| 26 | +npx hardhat compile |
9 | 27 | npx hardhat test |
10 | | -REPORT_GAS=true npx hardhat test |
11 | | -npx hardhat node |
12 | | -npx hardhat ignition deploy ./ignition/modules/Lock.js |
13 | 28 | ``` |
| 29 | + |
| 30 | +## Certificate life-cycle operations |
| 31 | + |
| 32 | +The registry is driven by a small set of operations, each implemented as a script |
| 33 | +under `scripts/`: |
| 34 | + |
| 35 | +| Operation | Script / function | When | |
| 36 | +|-----------|-------------------|------| |
| 37 | +| Deploy OnchainID suite | `suites/OID.js` → `deployOnchainIDSuite` | once | |
| 38 | +| Deploy T-REX suite | `suites/TREX.js` → `deployTrexSuite` | once | |
| 39 | +| Deploy a claim issuer (institution) | `claimIssuer/deploy-claim-issuer.js` → `deployClaimIssuer` | per institution | |
| 40 | +| Accredit an issuer (add to the trusted-issuer registry) | `addTrustedIssuer` (inside `deployClaimIssuer`) | per institution | |
| 41 | +| Create a student identity | `identities/deploy-identity.js` → `deployIdentity` | per student | |
| 42 | +| Authorise an issuer key on an identity | `claimIssuer/addKeyToIdentity.js` → `addKeyToIdentity` | per (student, issuer) | |
| 43 | +| Issue a certificate (add claims) | `claims/add-claim.js` → `addClaim` | per claim | |
| 44 | +| Update / revoke a certificate | `addClaim` again (emits `ClaimChanged`) | per correction | |
| 45 | +| Validate a certificate | `claims/getClaimsByTopic.js` + `utils/encryption/hash.js` | per verification | |
| 46 | + |
| 47 | +A claim is keyed by `claimId = keccak256(issuer, topic)`. Re-issuing the same |
| 48 | +`(issuer, topic)` overwrites the existing claim in place (emitting `ClaimChanged`) |
| 49 | +rather than adding a second one — this is the update/revocation path. Holding |
| 50 | +several claims of the *same* topic on one identity therefore requires distinct |
| 51 | +issuers. |
| 52 | + |
| 53 | +## Benchmarks (`eval/`) |
| 54 | + |
| 55 | +The `eval/` directory contains scripts that measure the gas, cost, scalability, |
| 56 | +and latency of the operations above. They are plain `hardhat run` scripts and |
| 57 | +append one CSV row per measurement under `eval/results/`. |
| 58 | + |
| 59 | +Gas is deterministic (identical bytecode + input + state ⇒ identical gas on any |
| 60 | +EVM chain), so gas is collected on the local Hardhat network. Fiat cost depends |
| 61 | +on the gas price and exchange rate, so on the local network the cost columns are |
| 62 | +≈0 and are meant to be computed in post-processing from `gasUsed`; against a real |
| 63 | +network they are populated from the live fee data. Wall-clock latency is only |
| 64 | +meaningful against a network with real block times (a testnet or private net). |
| 65 | + |
| 66 | +### Scripts |
| 67 | + |
| 68 | +- **`measure.js`** — per-operation gas, cost, and latency for each life-cycle |
| 69 | + operation, plus contract deployment gas and bytecode size (checked against the |
| 70 | + **EIP-170** 24 576-byte limit). Covers `deployIdentity`, `addClaim` (cold and |
| 71 | + warm), `addKeyToIdentity`, `addTrustedIssuer`, `deployClaimIssuer`, the contract |
| 72 | + deployments, and the validation read path. |
| 73 | +- **`baseline.js`** — the same metrics for a minimal hash-anchoring registry |
| 74 | + (`contracts/config/HashAnchor.sol`: store a certificate's SHA-256 digest, verify |
| 75 | + by lookup). Useful as a lower-bound comparison for the identity + compliance |
| 76 | + layer. |
| 77 | +- **`sweeps.js`** — scalability sweeps: issuer-resolution cost as the |
| 78 | + trusted-issuer registry grows, and `addClaim` gas + validation read latency as |
| 79 | + the number of claims on one identity grows. Writes raw and summarised |
| 80 | + (median/p95/min/max) CSVs. |
| 81 | +- **`latency.js`** — end-to-end issuance and validation latency against a live |
| 82 | + network, intended for Sepolia (or a private net). Uses a single funded key. |
| 83 | + |
| 84 | +### Running |
| 85 | + |
| 86 | +All commands run from this directory (`code/services/ethereum`): |
| 87 | + |
| 88 | +```sh |
| 89 | +# Per-operation gas + cost, and contract sizes (local) -> eval/results/measure.csv |
| 90 | +REPS=30 ETH_USD=3000 npx hardhat run eval/measure.js |
| 91 | + |
| 92 | +# Hash-anchoring baseline, same methodology (local) -> eval/results/baseline.csv |
| 93 | +REPS=30 ETH_USD=3000 npx hardhat run eval/baseline.js |
| 94 | + |
| 95 | +# Scalability sweeps (local) |
| 96 | +# -> eval/results/sweep-*.csv (raw) + eval/results/sweep-*-summary.csv |
| 97 | +REPS=10 STEP=5 MAX_CLAIMS=30 npx hardhat run eval/sweeps.js |
| 98 | + |
| 99 | +# End-to-end latency on a live network (needs a funded key + RPC endpoint) |
| 100 | +SEPOLIA_RPC=<your-rpc-url> PRIVATE_KEY=<funded-sepolia-key> \ |
| 101 | + REPS=30 npx hardhat run eval/latency.js --network sepolia |
| 102 | +``` |
| 103 | + |
| 104 | +Common environment variables: `REPS` (samples per operation), `ETH_USD` (rate |
| 105 | +used for the cost columns), `STEP` / `MAX_ISSUERS` / `MAX_CLAIMS` (sweep sizes), |
| 106 | +`CONFIRMATIONS` (confirmations to wait for in `latency.js`). |
| 107 | + |
| 108 | +All output CSVs share the schema |
| 109 | +`iso,network,operation,run,gasUsed,gasPriceGwei,costEth,costUsd,latencyMs,block,params`, |
| 110 | +one row per run. |
| 111 | + |
| 112 | +### Notes |
| 113 | + |
| 114 | +- **Scripts append.** Each run appends to its CSV; archive or delete the old file |
| 115 | + first if you want a clean dataset. |
| 116 | +- **Cold vs. warm storage.** The first write to a storage slot is far more |
| 117 | + expensive than a rewrite. `addClaim` on a new `(issuer, topic)` emits |
| 118 | + `ClaimAdded` (cold); repeating it emits `ClaimChanged` (warm, the update path). |
| 119 | + `measure.js` records both as `addClaim:cold` / `addClaim:warm`. It replicates |
| 120 | + the on-chain `addClaim` call in a local `buildClaimTx` helper because |
| 121 | + `scripts/claims/add-claim.js` returns the event args rather than the |
| 122 | + transaction — keep the two in sync if `add-claim.js` changes. |
| 123 | +- **Cold-loop signer cap.** `measure.js`'s `addClaim:cold` loop needs one funded |
| 124 | + signer per run and reserves signers 0–2, so it caps at 17 with Hardhat's default |
| 125 | + 20 accounts (it logs a warning when it caps below `REPS`). Add |
| 126 | + `accounts: { count: 33 }` to the `hardhat` network in `hardhat.config.js` for a |
| 127 | + full 30-run cold loop. |
| 128 | +- **Trusted-issuer registry limit.** The T-REX `TrustedIssuersRegistry` reverts |
| 129 | + beyond 50 issuers (`require(_trustedIssuers.length < 50)`), so `sweeps.js` clamps |
| 130 | + the registry sweep to 49. |
| 131 | +- **Claims-per-identity sweep.** Because `claimId = keccak256(issuer, topic)`, N |
| 132 | + claims under one topic need N distinct issuers; the sweep deploys real |
| 133 | + `ClaimIssuer` contracts, since `Identity.addClaim` validates external issuers |
| 134 | + on-chain via `isClaimValid`. |
| 135 | +- **`deployFullTREXSuiteFixture` is local-only.** It fires some setup transactions |
| 136 | + without awaiting confirmation, which is fine on the auto-mining local net but |
| 137 | + races on a live network; `latency.js` therefore builds only what it needs and |
| 138 | + awaits every transaction. |
| 139 | + |
| 140 | +## Static analysis (Slither) |
| 141 | + |
| 142 | +Security-relevant static analysis is run with |
| 143 | +[Slither](https://github.com/crytic/slither) over the contracts. Slither invokes |
| 144 | +`npx hardhat compile` under the hood and writes its report to **stderr**, so |
| 145 | +redirect it to a file: |
| 146 | + |
| 147 | +```sh |
| 148 | +# one-time: create a Python virtual environment with Slither |
| 149 | +python3 -m venv local |
| 150 | +source local/bin/activate |
| 151 | +pip install slither-analyzer # tested with Slither 0.11.5 on Python 3.13 |
| 152 | + |
| 153 | +# full detector report and a human-readable summary |
| 154 | +slither . 2> eval/slither.txt |
| 155 | +slither . --print human-summary 2> eval/slither-summary.txt |
| 156 | +``` |
| 157 | + |
| 158 | +The core OnchainID / T-REX contracts come from audited upstream |
| 159 | +implementations, so findings are triaged against this project's additions |
| 160 | +(`contracts/config/FileStorage.sol`, `contracts/config/HashAnchor.sol`, and the |
| 161 | +deployment scripts). The committed reference outputs are in `eval/slither.txt` and |
| 162 | +`eval/slither-summary.txt`. |
| 163 | + |
| 164 | +The negative tests under `test/security/` complement the static analysis by |
| 165 | +asserting the access-control guards hold — e.g. a non-trusted issuer calling |
| 166 | +`addClaim` reverts (the `isTrustedIssuer || isSelfSigner` guard in |
| 167 | +`add-claim.js`), a claim written without an authorised key on the identity |
| 168 | +reverts, and a claim with a bad issuer signature does not validate. |
| 169 | + |
| 170 | +## Environment |
| 171 | + |
| 172 | +Gas figures depend on the toolchain, so the pinned versions are worth recording |
| 173 | +when comparing runs: |
| 174 | + |
| 175 | +- Solidity **0.8.17 / 0.8.20**, optimizer **enabled**, **`runs = 200`** |
| 176 | + (`hardhat.config.js`). |
| 177 | +- `hardhat ^2.22.5`, `ethers ^6.13.1`, `@onchain-id/identity-sdk ^1.7.1`, |
| 178 | + `@openzeppelin/contracts ^4.8.3`, `crypto-js ^4.2.0`. |
| 179 | +- Networks: local `hardhat` (`chainId 31337`, default) for gas and sweeps; |
| 180 | + `sepolia` for latency (set `SEPOLIA_RPC` and `PRIVATE_KEY`). |
0 commit comments