# Aere Network contracts: continuous verification # # ───────────────────────────────────────────────────────────────────────────── # WHY THIS FILE IS AT .github/workflows/ AND NOT .gitea/workflows/ # ───────────────────────────────────────────────────────────────────────────── # The canonical host is our own Gitea instance at git.aere.network. Gitea Actions # scans BOTH `.gitea/workflows/` and `.github/workflows/`, and prefers `.gitea/` # only when both exist. GitHub Actions scans `.github/workflows/` only. # Putting the single source of truth at `.github/workflows/ci.yml` therefore # means the same unmodified file runs on both hosts. That is the point: a # stranger who mirrors this repo to GitHub gets the same pipeline with zero # edits, and can compare their run against ours. # # Portability notes for anyone porting this elsewhere: # * `actions/checkout@v4`, `actions/setup-node@v4` and # `actions/upload-artifact@v3` all resolve on Gitea Actions, which proxies # to github.com for action resolution by default. # * upload-artifact is pinned at v3, not v4. Gitea's artifact backend # implements the v3 upload protocol; v4 changed it and fails on Gitea at the # time of writing. On GitHub, v3 still runs. If you are GitHub-only, v4 is # fine. # * Gitea runners need a label matching `runs-on`. Ours registers # `ubuntu-22.04`. If your runner registers only `ubuntu-latest`, change the # three `runs-on:` lines. # # ───────────────────────────────────────────────────────────────────────────── # HONESTY STATEMENT: DOES THIS PIPELINE CURRENTLY PASS? # ───────────────────────────────────────────────────────────────────────────── # Recorded here rather than in a README so it cannot drift away from the file it # describes. Status as of 2026-07-20, measured on the maintainer's machine # (Windows 11, Node v24.14.1, npm 11.11.0), not assumed: # # job: build-and-test .......... SEE STATUS BLOCK BELOW (filled from a real run) # job: pqc-kat ................. SEE STATUS BLOCK BELOW # job: coverage ................ RUNS, but produces a SCOPED number, not a # whole-repo number. See the long comment on # that job. This is a measured limitation of a # 16 GB runner, not a choice to look good. # # What this pipeline deliberately does NOT do, because each would be a way of # being green while being wrong: # * no `continue-on-error` on any compile, test or KAT step; # * no `|| true` swallowing a non-zero exit anywhere in a gating step; # * no coverage threshold invented below the real number to manufacture a # pass. The one threshold that exists is set AT a measured value and exists # to catch regression; # * no test file excluded from the suite for being inconvenient. `skipFiles` # in .solcover.js is empty; # * `npm ci` (not `npm install`), so the lockfile is authoritative and a # drifting transitive dependency fails the build instead of silently # changing what was tested. # # If the suite goes red, the correct response is to fix the suite. Editing this # file to make it green again is the failure mode this file exists to prevent. name: contracts-ci on: push: pull_request: workflow_dispatch: # Least privilege. This pipeline reads code and writes artifacts. Nothing else. permissions: contents: read # ───────────────────────────────────────────────────────────────────────────── # THE SECRETS BOUNDARY # ───────────────────────────────────────────────────────────────────────────── # This is a public CI on a public repository. It has NO access to, and must # never be given access to: # # * validator signing keys for the 7 chain-2800 validators; # * any deploy key, funded account private key, or mnemonic with a balance; # * infrastructure credentials (SSH, Hetzner API, DNS, TLS private keys); # * RPC admin or any authenticated write endpoint; # * the explorer, relayer or oracle service credentials. # # The `env:` block below is exhaustive and contains no `secrets.*` reference. # That is verifiable by reading this file: grep it for `secrets.` and you get # nothing. A fork's pull request runs the identical pipeline with the identical # (empty) credential surface. # # WHY A PUBLIC CI STRUCTURALLY CANNOT TOUCH THE LIVE CHAIN, beyond just not # holding keys: # 1. Every job runs against an in-process Hardhat EVM at chainId 31337. It # never dials chain 2800. # 2. The only account material anywhere in the test path is the well-known # public Hardhat mnemonic ("test test test ... junk"), which holds nothing # on any real network. # 3. Writing to chain 2800 requires a signature from a funded key. No such key # exists in this repository, in this pipeline, or in the runner # environment, so a compromised workflow file, a malicious dependency, or a # hostile pull request still cannot produce a valid mainnet transaction. # 4. Deployment is a separate, human-gated, offline procedure. It is not # automated from CI by design, and moving it into CI would be a downgrade. # # Naming this boundary is part of the argument, not a caveat to it. "Radical # open source" means every number here is reproducible by a stranger; it does # not mean the keys are open. Linux publishes all of its code and none of its # signing keys. env: # Coverage instrumentation is memory-hungry. Raise V8's old-space so the # sharded run has room. NOTE: this bounds only the V8 old-space heap; the bulk # of coverage's growth is Hardhat's in-process EVM state plus the # per-statement hit maps, which live OUTSIDE this budget. Do not expect this # setting alone to make a whole-repo run fit. NODE_OPTIONS: --max-old-space-size=6144 # Never let a stray config or test reach a real endpoint from CI. AERE_RPC_URL: http://127.0.0.1:0 jobs: # ─────────────────────────────────────────────────────────────────────────── # 1. THE GATE. Clean-checkout install, compile, full suite. No exceptions. # ─────────────────────────────────────────────────────────────────────────── build-and-test: name: Install (lockfile) → compile → full test suite runs-on: ubuntu-22.04 timeout-minutes: 90 steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: # Pinned to the version the numbers in REPRODUCE.md were measured on. node-version: '24.14.1' cache: 'npm' - name: Record toolchain versions # Printed so any run can be compared against any other run. A number is # only reproducible if you know what produced it. run: | echo "node: $(node --version)" echo "npm: $(npm --version)" echo "os: $(uname -a)" echo "mem: $(free -g | awk '/^Mem:/{print $2" GB"}')" echo "cores: $(nproc)" - name: Install from the lockfile # `npm ci` NOT `npm install`. ci deletes node_modules and installs the # exact lockfile tree, failing if package.json and package-lock.json # disagree. That is precisely what an outsider cloning this repo does, # and it is the step that catches "works because of what is already on # my machine". run: npm ci - name: Compile contracts run: npx hardhat compile - name: Full test suite # THE GATE. No continue-on-error. A single failing test fails the run. run: npx hardhat test - name: Upload compiled artifacts if: always() uses: actions/upload-artifact@v3 with: name: build-artifacts path: | artifacts/ retention-days: 30 # ─────────────────────────────────────────────────────────────────────────── # 2. PQC KNOWN-ANSWER TESTS, isolated so a crypto regression is legible. # ─────────────────────────────────────────────────────────────────────────── # # These run inside the full suite too. They are ALSO run alone, because # "did the post-quantum verifiers still match the official NIST vectors" is # the single question this project is most often asked, and burying its answer # in a 1,400-line log is a bad answer. # # SCOPE HONESTY, because this is the most misread part of the project: # * These KATs exercise the SOLIDITY verifier implementations against the # official NIST reference vectors (falcon512-KAT.rsp / falcon1024-KAT.rsp, # FIPS 202 SHAKE256). # * They run on the local Hardhat EVM, which has NO Aere precompiles. The # precompile-backed verification paths (0x0AE1 to 0x0AE5 live on chain # 2800; 0x0AE6 ML-KEM-768 and 0x0AE7 HashToPoint are TESTNET-ONLY) cannot # be exercised here and are therefore NOT covered by this job. Verifying # those requires a precompile-bearing devnet. Do not read a green tick # here as evidence about the precompiles. # * Aere consensus is CLASSICAL secp256k1 ECDSA QBFT. Post-quantum # cryptography exists at the signature, precompile, account and transport # layers only. Nothing in this job says anything about consensus. pqc-kat: name: PQC known-answer tests (NIST vectors) runs-on: ubuntu-22.04 timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: '24.14.1' cache: 'npm' - name: Install from the lockfile run: npm ci - name: Compile contracts run: npx hardhat compile - name: Falcon / SPHINCS+ / ML-DSA known-answer tests # No continue-on-error. A KAT mismatch is a hard failure: it means an # implementation stopped agreeing with the NIST reference. run: | npx hardhat test \ test/AereFalcon512Verifier.test.js \ test/AereFalcon1024Verifier.test.js \ test/AereSphincsVerifier.test.js \ test/AerePQCMessageVerifier.test.js \ test/AereCryptoRegistry.test.js \ test/AereCryptoRegistry.agility.test.js \ test/AereCryptoRegistry.newprecompiles.test.js - name: Upload KAT vectors used if: always() uses: actions/upload-artifact@v3 with: name: pqc-kat-vectors path: | test/falcon512_kat0.json test/falcon1024_kat0.json retention-days: 90 # ─────────────────────────────────────────────────────────────────────────── # 3. COVERAGE. Read the scope statement before quoting any number from here. # ─────────────────────────────────────────────────────────────────────────── # # WHAT THIS JOB PRODUCES: a SCOPED coverage number over a named cluster of # contracts, plus the raw istanbul JSON as an artifact so anyone can recompute # it. It does NOT produce a whole-repository coverage percentage. # # WHY NOT, stated plainly rather than hidden behind a green tick: # A single-process whole-repo coverage run does not fit in a standard CI # runner's memory. Measured on a 15.78 GB / 33.78 GB-commit box, the process # reached the entire commit limit and was killed by the OS during the TEST # phase. The COMPILE phase is solved: all contracts instrument and compile, # and zero contracts are excluded from instrumentation. The remaining blocker # is memory during test execution, caused by per-statement hit maps for the # on-chain crypto suites, which execute tens of millions of statements per # verify. # # The path to a real whole-repo number is sharding: run coverage over subsets # with --testfiles, then merge the per-shard istanbul JSONs. That merge is # arithmetically valid because instrumentation is deterministic over identical # sources, so every shard emits identical statement/branch/function maps and # the per-counter arrays sum. Until that full sharded run completes end to # end, the honest whole-repo figure is NOT MEASURED, and this pipeline says so # rather than substituting a scoped number for it. # # ANY figure of the "99.8% coverage" kind that has ever been attached to this # project was never produced by a coverage tool. It was a target written as # though achieved. It is not a claim this repository makes. The numbers below # are the real ones, and they are lower. coverage: name: Coverage (scoped, with artifacts) runs-on: ubuntu-22.04 timeout-minutes: 120 steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: '24.14.1' cache: 'npm' - name: Install from the lockfile run: npm ci - name: Coverage over the economic-core cluster # Scoped by TEST FILE, not by skipFiles. This matters: skipFiles would # shrink the DENOMINATOR and inflate the percentage. --testfiles leaves # every contract instrumented and simply runs fewer tests, so the number # produced is an honest "what these tests reach", measured against the # full instrumented denominator. # # No continue-on-error: if a test in this scope fails, coverage fails. run: | npx hardhat coverage \ --config hardhat.config.coverage.js \ --testfiles "{test/aere-fee-burn-vault.test.js,test/aere-sink-branch-coverage.test.js,test/saere-v2-fix.test.js,test/aeresink-conservation-invariant.test.js}" - name: Print the measured summary run: | echo "── Coverage summary (istanbul) ─────────────────────────────" cat coverage/coverage-summary.json 2>/dev/null \ || echo "coverage-summary.json not emitted; see the artifact for coverage.json" - name: Upload the raw coverage report # `if: always()` is used ONLY on artifact-upload steps, so that a FAILED # run still hands you its evidence. It is never used on a step that # decides pass or fail. That distinction is the whole difference between # a debuggable pipeline and a dishonest one. if: always() uses: actions/upload-artifact@v3 with: name: coverage-report path: | coverage/ coverage.json retention-days: 90