aere-contracts/.github/workflows/ci.yml
Aere Network acac2f00a6
Some checks failed
contracts-ci / Install (lockfile) → compile → full test suite (push) Has been cancelled
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Has been cancelled
contracts-ci / PQC known-answer tests (NIST vectors) (push) Has been cancelled
contracts-ci / Coverage (scoped, with artifacts) (push) Has been cancelled
The unpublished line of work joins the sanitized public line
The published line and the local line had no common ancestor: the public one
carried the redaction pass, the local one carried three weeks of corrections
that never shipped. This commit ports the local work onto the public line,
keeps every public redaction, and extends the same discretion to seven client
mentions that were still named in published comments.

Carried: LICENSE year and LICENSING.md; the measured burn figures replacing
the deflation claim (the vault holds ~0.137 AERE of 2.8 billion, and burn is
a share of validator coinbase revenue, which is zero today); 'audited' removed
from next to Bouncy Castle; citation paths rewritten to published form with
CITATIONS-UNRESOLVED.md remeasured 2026-08-11; VERIFY-POLICY.md; slashing and
ownership comments brought down to what the code does; the AerePyth repair;
the shutter test helper the tests cite; runnable package.json entries; the CI
file split into a GitHub/Gitea twin pair with a real measured test-run status;
and the .gitignore hardening written after a compiled artifact leaked a local
path in a sibling repository. A false '2-of-3 multisig' description of the
owner account is corrected to what the chain measures: an externally owned
account. The self-audit findings catalog stays unpublished pending an explicit
decision.
2026-08-15 13:59:30 +03:00

481 lines
25 KiB
YAML

# 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.
#
# THIS FILE IS THE GITHUB-MIRROR HALF OF A TWO-FILE PAIR. Its twin is
# `.gitea/workflows/ci.yml`, which is what git.aere.network actually runs.
#
# CORRECTION, 2026-07-20. An earlier revision of this file was a SINGLE file
# meant to serve both hosts, and justified pinning `actions/upload-artifact` at
# v3 with the note "On GitHub, v3 still runs." THAT NOTE WAS WRONG. GitHub
# retired the v3 artifact backend on 2025-01-30 and `upload-artifact@v3` now
# hard-fails on GitHub-hosted runners. So the single-file design did not deliver
# the portability it claimed; it delivered a pipeline that fails on GitHub for a
# reason that has nothing to do with our code. A CI that goes red for a tooling
# reason is worse than one that goes red for a real reason, because it teaches
# people to ignore the colour. Splitting the file is the fix.
#
# The two files are identical except for the `upload-artifact` major version and
# their header notes. Drift is checkable in one command:
#
# diff <(grep -v '^\s*#' .gitea/workflows/ci.yml | grep -v '^$') \
# <(grep -v '^\s*#' .github/workflows/ci.yml | grep -v '^$')
#
# Note `^\s*#` and not `^#`: indented comments must be stripped too, or the diff
# drowns in commentary and stops being a usable check.
#
# MEASURED 2026-07-20: exactly THREE differences, all the `upload-artifact@v3` /
# `@v4` line. `runs-on:` is currently identical in both files (`ubuntu-22.04`).
# Anything else in that diff means one host is no longer running what we say.
#
# Portability notes for anyone porting this elsewhere:
# * `actions/checkout@v4`, `actions/setup-node@v4` and
# `actions/upload-artifact@v4` all resolve on GitHub. Gitea Actions proxies
# to github.com for action resolution by default, but its artifact backend
# implements the v3 protocol, which is why the `.gitea/` twin pins v3.
# * Runners need a label matching `runs-on`. If your runner registers only
# `ubuntu-latest`, change the four `runs-on:` lines.
#
# ─────────────────────────────────────────────────────────────────────────────
# HONESTY STATEMENT: DOES THIS PIPELINE CURRENTLY PASS?
# ─────────────────────────────────────────────────────────────────────────────
#
# NO. `build-and-test` IS CURRENTLY RED. This is written here, at the top of the
# file, because the alternative (shipping a pipeline tuned to look green) is the
# specific failure this file exists to prevent.
#
# Status recorded from an actual run on 2026-07-20, not assumed. Command:
#
# NODE_OPTIONS=--max-old-space-size=6144 npx hardhat test
#
# on Windows 11, Node v24.14.1, npm 11.11.0, 15.78 GB RAM. Result:
#
# 1,448 tests passing
# 9 tests failing
# run ABORTED before finishing, exit 127, last line of output:
# `memory allocation of 3623878656 bytes failed`
#
# Note carefully: because the process was killed, mocha never printed its
# summary. So even "1,448 passing / 9 failing" is a PARTIAL count taken by
# counting result markers in the log. It is not the suite's totals, and the true
# totals are currently unknown. We are not going to round that into a
# comfortable number.
#
# TWO SEPARATE PROBLEMS, deliberately not conflated:
#
# (1) MEMORY. The full suite does not fit in ~16 GB. The on-chain crypto
# suites (Falcon, ML-DSA, SPHINCS+) are enormously expensive to execute in
# an in-process EVM. A standard GitHub or Gitea runner has 16 GB, so
# `build-and-test` is EXPECTED to die the same way until the suite is
# sharded across jobs. That work is not done. The job is left in its
# honest failing state rather than trimmed until it fits.
#
# (2) NINE REAL FAILURES. Grouped by cause:
#
# * 6 in `AereEthLightClient (real EIP-2537 BLS, Prague)`. MISCONFIGURED
# RUN, not a contract defect. That suite lives in test/interop/ and has
# its own config (hardhat.config.interop.js) pinning the `prague`
# hardfork, because it needs the EIP-2537 BLS12-381 precompiles
# (0x0b..0x11). The default config pins `cancun`, where those
# precompiles do not exist, and `npx hardhat test` globs test/
# recursively and drags the interop suite in under the wrong hardfork.
# MEASURED: re-running that suite with `--config hardhat.config.interop.js`
# turns "hashes a message to G2 identically to noble hashToCurve" from
# failing to PASSING. The `interop` job below therefore runs it the
# correct way. This is not skipping an inconvenient suite; it is running
# each suite under the config it was written for, and the interop job is
# itself a hard gate.
#
# * ...but that same corrected run then dies with a SEGMENTATION FAULT in
# node on `accepts a valid full-participation update and advances
# finality`. That is a hard crash in the EVM implementation's EIP-2537
# path, not something the Solidity can be blamed for and not something
# a config flag fixes. It is an open, unresolved blocker. The `interop`
# job is red because of it.
#
# * 3 others, causes NOT yet diagnosed, listed so they are not lost:
# - AereCompliancePool fuzz: "survives 1500 randomized actions with
# all invariants intact"
# - INVARIANT AereGovernorV2: "post-creation locks never inflate a
# proposal's quorum; bounds + turnout rule hold"
# - INVARIANT AereLendingMarket: "drives multi-actor
# supply/borrow/repay/withdraw/liquidate across price moves"
# Because the run was killed before mocha's summary, the assertion text
# for these three was never printed. We have their titles and nothing
# more. Diagnosing them is outstanding work, and until it is done we
# cannot say whether they are product defects or run artifacts. We are
# not going to guess in public.
#
# job: build-and-test .......... RED (memory abort + 9 failures, above)
# job: interop ................. RED (segfault in the EIP-2537 BLS path)
# job: pqc-kat ................. GREEN. MEASURED 2026-07-20 in a CLEAN
# directory (fresh `npm ci`, fresh compile),
# running this job's exact command in two
# parts: 60 passing (7s) exit 0, then 41
# passing (27s) exit 0. 101 passing, 0 failing,
# 0 pending. Includes the official NIST
# Falcon-512, Falcon-1024 and SPHINCS+ (FIPS
# 205 SLH-DSA ACVP) vectors, asserted in BOTH
# directions: genuine signature ACCEPTED, and
# tampered-signature / tampered-message /
# wrong-public-key all REJECTED.
# job: coverage ................ RED, most likely. MEASURED 2026-07-20: the
# coverage run failed at the COMPILE phase with
# `std::bad_alloc` out of solc itself, exit 1,
# after ~45 minutes, with the node process
# observed at 16.04 GB private bytes on a
# 15.78 GB box. We have NO coverage number
# from today. None. Not a low one.
#
# Two corrections worth carrying forward:
# (a) an earlier internal note recorded that the
# instrumented whole-repo COMPILE was solved.
# It did not reproduce.
# (b) `--testfiles` does NOT reduce compile
# cost. solidity-coverage instruments and
# compiles EVERY contract regardless;
# --testfiles only narrows which tests execute
# afterwards. So scoping by test file cannot
# rescue a run that dies during compilation.
# This job is left doing the real thing and
# failing honestly, rather than made green by
# lowering a threshold, shrinking the
# denominator via skipFiles, or adding
# continue-on-error.
#
# 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, cloud provider 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@v4
with:
name: build-artifacts
path: |
artifacts/
retention-days: 30
# ───────────────────────────────────────────────────────────────────────────
# 2. ETHEREUM INTEROP, run under the config it was actually written for.
# ───────────────────────────────────────────────────────────────────────────
#
# test/interop/ needs the `prague` hardfork for the EIP-2537 BLS12-381
# precompiles (0x0b..0x11). The main config pins `cancun` to keep every other
# contract's bytecode byte-stable, so this suite ships its own config.
#
# WHY THIS IS A SEPARATE JOB AND NOT A SKIP. Running these tests under the
# default cancun config is simply the wrong runner setup, and it produces 6
# failures that say nothing about the contracts. Giving the suite its correct
# config is the fix. It is NOT an exemption: this job gates like any other and
# is currently RED (see the header: a segfault in the EVM's EIP-2537 path).
# Leaving it red is the point. If we wanted a green wall we would have deleted
# this job.
interop:
name: Ethereum interop (EIP-2537 BLS, prague hardfork)
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: Interop suite (own config, prague hardfork)
# No continue-on-error. This job is expected to fail today and that
# failure is the accurate signal.
run: npx hardhat test --config hardhat.config.interop.js
# ───────────────────────────────────────────────────────────────────────────
# 3. 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@v4
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 when the full test set was in scope.
#
# CONTRADICTION RESOLVED, 2026-07-20. An earlier revision of this comment
# asserted "The COMPILE phase is solved: all contracts instrument and
# compile", while this same file's header recorded a compile-phase
# `std::bad_alloc`. Both could not be true, and shipping a file that
# contradicts itself about its own headline number is exactly the failure this
# pipeline exists to prevent. What is actually true, measured rather than
# asserted:
#
# * The instrumented whole-repo compile is NOT reliable. It succeeded in an
# earlier session and failed on 2026-07-20 with `std::bad_alloc` out of
# solc after ~45 minutes. Same machine, same tree. It is
# memory-pressure-dependent, not deterministic, and a 16 GB runner should
# be expected to hit the failing side of that coin.
# * Zero contracts are excluded from instrumentation. `.solcover.js` has an
# empty `skipFiles`. MEASURED 2026-07-20: 294 files instrument.
# * When the instrumented artifacts are already cached, the TEST phase and
# report generation complete cleanly at small test scope. MEASURED
# 2026-07-20, exit 0, full report emitted over all 294 files.
#
# So the blocker is the cold instrumented compile, and secondarily test-phase
# memory at large scope. Not one single cause. Saying so costs nothing and
# keeps the file honest against its own header.
#
# 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@v4
with:
name: coverage-report
path: |
coverage/
coverage.json
retention-days: 90