The unpublished line of work joins the sanitized public line
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
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 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.
This commit is contained in:
parent
6137155ab6
commit
acac2f00a6
520
.gitea/workflows/ci.yml
Normal file
520
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,520 @@
|
||||
# Aere Network contracts: continuous verification (GITEA ACTIONS)
|
||||
#
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# WHY THIS FILE EXISTS, AND WHY IT IS A SEPARATE FILE
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# The canonical host is our own Gitea instance at git.aere.network. Gitea
|
||||
# Actions scans BOTH `.gitea/workflows/` and `.github/workflows/`, and prefers
|
||||
# `.gitea/workflows/` when both are present. GitHub Actions scans
|
||||
# `.github/workflows/` only. (SPEC: Gitea Actions documentation.)
|
||||
#
|
||||
# An earlier version of this repository relied on that overlap to run ONE file
|
||||
# on both hosts, and justified pinning `actions/upload-artifact` at v3 with the
|
||||
# note "On GitHub, v3 still runs."
|
||||
#
|
||||
# THAT NOTE IS OUT OF DATE AND THE SINGLE-FILE DESIGN IS THEREFORE BROKEN.
|
||||
# GitHub retired the v3 artifact backend on 2025-01-30; `upload-artifact@v3`
|
||||
# now hard-fails on GitHub-hosted runners. So the single-file design did not
|
||||
# actually give portability, it gave a pipeline that fails on GitHub for a
|
||||
# reason having 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
|
||||
# trains people to ignore the colour.
|
||||
#
|
||||
# The split is the fix:
|
||||
#
|
||||
# .gitea/workflows/ci.yml (THIS FILE) -> runs on git.aere.network.
|
||||
# upload-artifact pinned at v3,
|
||||
# which is the protocol the Gitea
|
||||
# artifact backend implements.
|
||||
# .github/workflows/ci.yml -> runs on GitHub mirrors.
|
||||
# upload-artifact at v4.
|
||||
#
|
||||
# Because Gitea prefers `.gitea/`, each host runs exactly one file and each
|
||||
# file is correct for its host. Nothing is duplicated silently: the two files
|
||||
# are identical except for the artifact action version and these header notes.
|
||||
# Drift between them 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: that command returns exactly THREE differences, all of
|
||||
# them the `upload-artifact@v3` / `@v4` line. `runs-on:` is currently identical
|
||||
# in both files (`ubuntu-22.04`); it is called out separately below only because
|
||||
# it is the line you would change per host if your runner registers a different
|
||||
# label. If the diff ever returns anything other than those three lines, one of
|
||||
# the two hosts is no longer running what we claim it runs, and that is a bug.
|
||||
#
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# HONESTY STATEMENT: DOES THIS PIPELINE CURRENTLY PASS?
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# NO. Two of the four jobs are RED, deliberately, and will stay red until the
|
||||
# underlying problems are fixed rather than until the pipeline is edited.
|
||||
#
|
||||
# job: build-and-test .......... RED
|
||||
# job: interop ................. RED
|
||||
# job: pqc-kat ................. GREEN
|
||||
# job: coverage ................ RED
|
||||
#
|
||||
# Each status below is recorded from a command someone actually ran, with the
|
||||
# date, and is labelled MEASURED. Nothing here is assumed.
|
||||
#
|
||||
# ── build-and-test: RED ──────────────────────────────────────────────────────
|
||||
# MEASURED 2026-07-20, Windows 11, Node v24.14.1, npm 11.11.0, 15.78 GB RAM:
|
||||
#
|
||||
# NODE_OPTIONS=--max-old-space-size=6144 npx hardhat test
|
||||
#
|
||||
# 1,448 passing
|
||||
# 9 failing
|
||||
# run ABORTED, exit 127
|
||||
# last line: memory allocation of 3623878656 bytes failed
|
||||
#
|
||||
# Read the counts carefully. The process was killed, so mocha never printed its
|
||||
# summary. "1,448 passing / 9 failing" are result markers scraped from the log,
|
||||
# NOT the suite's totals. The suite's true totals are currently UNKNOWN. Quoting
|
||||
# 1,448 as "the number of passing tests" would be a smaller version of exactly
|
||||
# the fabrication this pipeline exists to correct.
|
||||
#
|
||||
# Two distinct causes, kept apart:
|
||||
#
|
||||
# (1) MEMORY. The full suite does not fit in ~16 GB. A standard Gitea or
|
||||
# GitHub runner has 16 GB, so this job is EXPECTED to die the same way
|
||||
# until the suite is sharded across jobs. That sharding is NOT done.
|
||||
#
|
||||
# (2) NINE FAILURES.
|
||||
# * 6 in `AereEthLightClient (real EIP-2537 BLS, Prague)`. A MISCONFIGURED
|
||||
# RUN, not a contract defect: that suite lives in test/interop/ and
|
||||
# ships its own config pinning the `prague` hardfork for the EIP-2537
|
||||
# BLS12-381 precompiles (0x0b..0x11), while the default config pins
|
||||
# `cancun` where those precompiles do not exist. `npx hardhat test`
|
||||
# globs test/ recursively and drags it in under the wrong hardfork.
|
||||
# MEASURED: under the correct config, one of those six becomes a PASS,
|
||||
# and the run then SEGFAULTS in the EVM's EIP-2537 path. The `interop`
|
||||
# job below runs it correctly and is red because of that segfault.
|
||||
# * 3 undiagnosed (AereCompliancePool fuzz, AereGovernorV2 quorum
|
||||
# invariant, AereLendingMarket solvency invariant). We have their titles
|
||||
# and no assertion text, because the run died before mocha's summary. We
|
||||
# do not know whether they are product defects or run artifacts, and we
|
||||
# are not going to guess in public.
|
||||
#
|
||||
# ── interop: RED ─────────────────────────────────────────────────────────────
|
||||
# Segmentation fault in node, inside the EVM implementation's EIP-2537 BLS
|
||||
# path, on `accepts a valid full-participation update and advances finality`.
|
||||
# A hard crash in the toolchain. Open and undiagnosed. Giving this suite its own
|
||||
# config is running it the way it was written, NOT an exemption: it gates like
|
||||
# every other job. If a green wall were the goal, this job would have been
|
||||
# deleted instead.
|
||||
#
|
||||
# ── pqc-kat: GREEN ───────────────────────────────────────────────────────────
|
||||
# MEASURED 2026-07-20, and INDEPENDENTLY RE-MEASURED the same day by a second
|
||||
# session running this job's exact commands, in two parts:
|
||||
#
|
||||
# part 1 (CryptoRegistry x3 + SPHINCS+): 60 passing (6s) exit 0
|
||||
# part 2 (Falcon-512/1024 + PQCMessage): 41 passing (28s) exit 0
|
||||
#
|
||||
# 101 passing, 0 failing, 0 pending. This is the one job that is green, and it
|
||||
# is green having been run twice rather than assumed once.
|
||||
#
|
||||
# ── coverage: RED ────────────────────────────────────────────────────────────
|
||||
# MEASURED 2026-07-20. The coverage run FAILED at the COMPILE phase:
|
||||
#
|
||||
# Unknown exception during compilation: Dynamic exception type:
|
||||
# class std::bad_alloc
|
||||
# Error in plugin solidity-coverage: HardhatError: HH600: Compilation failed
|
||||
#
|
||||
# exit 1, after roughly 45 minutes, node observed at 16.04 GB private bytes on a
|
||||
# 15.78 GB box. WE HAVE NO COVERAGE NUMBER FROM TODAY. Not a low one. None.
|
||||
#
|
||||
# CORRECTION CARRIED FORWARD, because an earlier copy of this file contradicted
|
||||
# itself on exactly this point and that is worth naming rather than quietly
|
||||
# patching. A previous revision's coverage-job comment asserted "The COMPILE
|
||||
# phase is solved: all contracts instrument and compile", while its own header
|
||||
# recorded a compile-phase std::bad_alloc. Both cannot be true. The header was
|
||||
# right and the job comment was a stale note from a 2026-07-19 run that did NOT
|
||||
# reproduce on 2026-07-20. An internal note that no longer reproduces is the
|
||||
# same species of problem as an unmeasured coverage percentage, only with a
|
||||
# shorter half-life.
|
||||
#
|
||||
# A SECOND CORRECTION: `--testfiles` does NOT reduce compile cost.
|
||||
# solidity-coverage instruments and compiles EVERY contract in the tree
|
||||
# regardless; `--testfiles` only narrows which tests execute AFTERWARDS. So
|
||||
# scoping by test file cannot rescue a run that dies during compilation. It
|
||||
# cannot, by construction. We made that mistake and are recording it so nobody
|
||||
# repeats it.
|
||||
#
|
||||
# THE COVERAGE FIGURE THIS PROJECT USED TO CARRY WAS FABRICATED. A "99.8 percent
|
||||
# statements / 95 percent branches" figure has been associated with Aere
|
||||
# Network. It was NEVER produced by a coverage tool. It was a target written as
|
||||
# though it had been achieved. It is not a claim this repository makes.
|
||||
# The honest replacement, in full:
|
||||
#
|
||||
# * Whole-repository coverage: NOT MEASURED. Not "approximately", not
|
||||
# "roughly 90 percent": not measured. The run does not complete.
|
||||
# * Best prior floor, dated 2026-07-19 and NOT reproduced on 2026-07-20:
|
||||
# 26.82% statements (2,585/9,638), 22.74% branches (2,088/9,184),
|
||||
# 23.25% functions (573/2,464), 27.21% lines (2,487/9,139), from a merge of
|
||||
# two completed shards covering 64 of 168 test files against the FULL
|
||||
# repository denominator. This is a floor from a minority of the test files.
|
||||
# It must never be quoted as the project's coverage figure in either
|
||||
# direction.
|
||||
# * Scoped economic-core cluster (7 contracts of ~285), same 2026-07-19
|
||||
# provenance, also not reproduced: 98.17% statements, 79.82% branches.
|
||||
# Seven contracts is not a repository.
|
||||
#
|
||||
# 26.82 that reproduces is worth more to this project than 99.8 that evaporates.
|
||||
#
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# WHAT THIS PIPELINE IS FORBIDDEN FROM DOING
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# These are the standard ways a CI file is made green while the software is
|
||||
# broken. None is used here, and every absence is checkable by reading the file:
|
||||
#
|
||||
# * No `continue-on-error` on any compile, test, KAT or coverage step.
|
||||
# * No `|| true` swallowing a non-zero exit in any gating step.
|
||||
# * No coverage threshold invented below the real number to manufacture a
|
||||
# pass. There is no threshold at all, because there is no real number yet.
|
||||
# * No suite skipped for being slow or inconvenient. `.solcover.js` has an
|
||||
# EMPTY `skipFiles`, so nothing is removed from the coverage denominator.
|
||||
# * `if: always()` appears ONLY on artifact-upload steps, so a failing run
|
||||
# still hands you its evidence. It never appears on a step that decides
|
||||
# pass or fail. That distinction is the entire difference between a
|
||||
# debuggable pipeline and a dishonest one.
|
||||
#
|
||||
# Verify the first two yourself, and note the exact patterns. A bare
|
||||
# `grep secrets.` returns hits because this file DISCUSSES secrets in prose; the
|
||||
# patterns below match the forms that would actually grant access or actually
|
||||
# suppress a failure:
|
||||
#
|
||||
# grep -c '\${{ *secrets\.' .gitea/workflows/ci.yml # expect 0
|
||||
# grep -cE '^\s*continue-on-error\s*:' .gitea/workflows/ci.yml # expect 0
|
||||
#
|
||||
# MEASURED 2026-07-20: both return 0.
|
||||
#
|
||||
# If the suite goes red, the correct response is to fix the suite. Editing this
|
||||
# file to restore green is precisely the failure this design prevents.
|
||||
|
||||
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
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# "Radical open source" invites this question immediately, so it is answered
|
||||
# here in full rather than as a footnote. Naming the boundary is part of the
|
||||
# credibility, not a caveat to it. Linux publishes all of its code and none of
|
||||
# its signing keys.
|
||||
#
|
||||
# WHAT THIS PIPELINE DOES NOT HAVE, AND MUST NEVER BE GIVEN:
|
||||
#
|
||||
# * validator signing keys for the 7 chain-2800 validators;
|
||||
# * any deploy key, funded account private key, or mnemonic with a balance;
|
||||
# * infrastructure credentials: SSH, hosting provider APIs, DNS, TLS private
|
||||
# keys;
|
||||
# * RPC admin, or any authenticated write endpoint;
|
||||
# * explorer, relayer or oracle service credentials.
|
||||
#
|
||||
# The `env:` block below is exhaustive and contains no `secrets.*` reference.
|
||||
# A pull request from a fork runs the identical pipeline against the identical
|
||||
# (empty) credential surface.
|
||||
#
|
||||
# SCOPE OF THAT CLAIM, stated precisely so it cannot be over-read. It covers
|
||||
# THIS workflow file. It is not a claim that no workflow anywhere in the wider
|
||||
# Aere monorepo references a secret: the internal `reproducible-besu-build.yml`
|
||||
# does reference a `WELLKNOWN_DEPLOY_TOKEN` used to publish a build digest to a
|
||||
# static `.well-known` path. That token publishes a hash to a website. It is not
|
||||
# a chain key, it cannot sign a transaction, and it is not present in this
|
||||
# published contracts repository at all. We are naming it because a reader who
|
||||
# greps only this file and trusts a blanket "no secrets anywhere" claim would
|
||||
# have been misled, and a boundary that survives only careless reading is not a
|
||||
# boundary.
|
||||
#
|
||||
# WHY A PUBLIC CI STRUCTURALLY CANNOT TOUCH THE LIVE CHAIN, beyond not holding
|
||||
# keys. Four independent reasons, any one of which is sufficient:
|
||||
#
|
||||
# 1. Every job runs against an in-process Hardhat EVM at chain ID 31337. It
|
||||
# never dials chain 2800. `AERE_RPC_URL` below is pinned to a dead local
|
||||
# address so a stray config cannot reach a real endpoint.
|
||||
# 2. The only account material anywhere in the test path is the well-known
|
||||
# public Hardhat test mnemonic ("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, this pipeline, or the runner environment. 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
|
||||
# deliberately NOT automated from CI. Moving it into CI would be a
|
||||
# downgrade in safety, not an improvement in convenience.
|
||||
#
|
||||
# Aere consensus is CLASSICAL secp256k1 ECDSA QBFT with 7 Foundation-operated
|
||||
# validators, f=2, quorum 5 of 7, 500 ms blocks (SPEC). Post-quantum
|
||||
# cryptography exists at the signature, precompile, account and transport layers
|
||||
# only. Nothing in this pipeline says anything about consensus, and no Aere
|
||||
# material should ever describe Aere as having post-quantum consensus.
|
||||
|
||||
env:
|
||||
# Bounds only the V8 old-space heap. The bulk of coverage's growth is
|
||||
# Hardhat's in-process EVM state plus per-statement hit maps, which live
|
||||
# OUTSIDE this budget. Do not expect this setting alone to make a whole-repo
|
||||
# coverage run fit; it does not, and section "coverage: RED" above is why.
|
||||
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
|
||||
# GITEA PORTABILITY: the runner must register a label matching this string.
|
||||
# Ours registers `ubuntu-22.04`. If your Gitea runner registers only
|
||||
# `ubuntu-latest`, change this line in all four jobs.
|
||||
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 every number in REPRODUCE.md was 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 outright 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 "it works because of
|
||||
# what was 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.
|
||||
# Currently RED: see the header. Leaving it red is the correct state.
|
||||
run: npx hardhat test
|
||||
|
||||
- name: Upload compiled artifacts
|
||||
if: always()
|
||||
# v3, not v4: the Gitea artifact backend implements the v3 upload
|
||||
# protocol. The GitHub twin of this file uses v4, because GitHub retired
|
||||
# v3 on 2025-01-30. This is the ONLY functional difference between the
|
||||
# two files.
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
artifacts/
|
||||
retention-days: 30
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────
|
||||
# 2. ETHEREUM INTEROP, run under the config it was actually written for.
|
||||
# ───────────────────────────────────────────────────────────────────────────
|
||||
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. 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 also run inside the full suite. They are ALSO run alone, because
|
||||
# "do the post-quantum verifiers still match the official NIST vectors" is the
|
||||
# question this project is asked most often, 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,
|
||||
# the FIPS 205 SLH-DSA ACVP vectors, the FIPS 202 SHAKE256 vector). The
|
||||
# vectors ship in this repository as test/falcon512_kat0.json and
|
||||
# test/falcon1024_kat0.json so you can diff them against the NIST
|
||||
# originals rather than trusting our copy.
|
||||
# * They assert BOTH directions, which is the part that matters: a genuine
|
||||
# NIST signature is ACCEPTED, and a single-bit-flipped signature, a
|
||||
# single-bit-flipped message, and a genuine signature under the wrong
|
||||
# public key are all REJECTED. An "accepts everything" verifier would pass
|
||||
# the first check and fail the rest.
|
||||
# * They run on the local Hardhat EVM, which has NO Aere precompiles. The
|
||||
# precompile-backed paths are therefore NOT covered by this job. A green
|
||||
# tick here is evidence about the Solidity implementations, not about the
|
||||
# deployed precompiles. Verifying those needs a precompile-bearing devnet,
|
||||
# which a public CI does not have.
|
||||
# * Precompile status is NOT uniform (SPEC): Falcon-512 0x0AE1,
|
||||
# Falcon-1024 0x0AE2, ML-DSA-44 0x0AE3, SLH-DSA-128s 0x0AE4 and
|
||||
# SHAKE256 0x0AE5 are live on mainnet from block 9,189,161. ML-KEM-768
|
||||
# 0x0AE6 and Falcon HashToPoint 0x0AE7 are TESTNET-ONLY and are NOT live
|
||||
# on mainnet. P-256 at 0x100 is live.
|
||||
# * The on-chain ZK verifiers are classical BN254, which is breakable by a
|
||||
# sufficiently large quantum computer. Post-quantum signature verification
|
||||
# does not make the ZK stack post-quantum.
|
||||
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
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────
|
||||
# 4. COVERAGE. Read the header's "coverage: RED" section before quoting any
|
||||
# number that comes out of this job.
|
||||
# ───────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# WHAT THIS JOB IS FOR: producing a real, checkable coverage number and the
|
||||
# raw istanbul JSON alongside it, so anyone can recompute the percentage from
|
||||
# the counters rather than take our word for the summary line.
|
||||
#
|
||||
# WHAT IT CURRENTLY DOES: fails. MEASURED 2026-07-20, it dies at the COMPILE
|
||||
# phase with std::bad_alloc out of solc itself, exit 1, on a 15.78 GB box.
|
||||
# It 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 IT WILL NOT PRODUCE EVEN WHEN IT RUNS: a whole-repository percentage.
|
||||
# A single-process whole-repo run does not fit a 16 GB runner. The route to a
|
||||
# real whole-repo number is sharding with --testfiles plus an istanbul merge.
|
||||
# That merge is arithmetically valid because instrumentation is deterministic
|
||||
# over identical sources, so every shard emits identical statement, branch and
|
||||
# function maps and the per-counter arrays sum. Keeping skipFiles empty in
|
||||
# every shard is what guarantees those maps match. Until that sharded run is
|
||||
# taken to completion end to end, the honest whole-repo figure is NOT
|
||||
# MEASURED, and this pipeline says so rather than substituting a scoped number
|
||||
# for it.
|
||||
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, never by skipFiles. This matters: skipFiles would
|
||||
# shrink the DENOMINATOR and inflate the percentage, which is the most
|
||||
# common way a coverage number gets quietly flattered. `--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.
|
||||
#
|
||||
# Note the limitation recorded in the header: --testfiles does NOT
|
||||
# reduce COMPILE cost, so this scoping does not rescue the run from its
|
||||
# current compile-phase OOM. It is here for when the compile is fixed.
|
||||
#
|
||||
# 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 a FAILED run
|
||||
# still hands you its evidence. It is never used on a step that decides
|
||||
# pass or fail.
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-report
|
||||
path: |
|
||||
coverage/
|
||||
coverage.json
|
||||
retention-days: 90
|
||||
230
.github/workflows/ci.yml
vendored
230
.github/workflows/ci.yml
vendored
@ -6,36 +6,147 @@
|
||||
# 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.
|
||||
#
|
||||
# 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@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.
|
||||
# `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?
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 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.
|
||||
# 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:
|
||||
@ -157,7 +268,7 @@ jobs:
|
||||
|
||||
- name: Upload compiled artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
@ -165,7 +276,45 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────
|
||||
# 2. PQC KNOWN-ANSWER TESTS, isolated so a crypto regression is legible.
|
||||
# 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
|
||||
@ -222,7 +371,7 @@ jobs:
|
||||
|
||||
- name: Upload KAT vectors used
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pqc-kat-vectors
|
||||
path: |
|
||||
@ -242,11 +391,30 @@ jobs:
|
||||
# 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.
|
||||
# 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
|
||||
@ -303,7 +471,7 @@ jobs:
|
||||
# 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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: |
|
||||
|
||||
40
.gitignore
vendored
40
.gitignore
vendored
@ -8,6 +8,19 @@ artifacts/
|
||||
# Coverage run outputs. Separate from the production build on purpose, so an
|
||||
# instrumented build can never be mistaken for, or overwrite, the real one.
|
||||
# See hardhat.config.coverage.js.
|
||||
#
|
||||
# `cov-out/` is the one that actually matters and it was MISSING from this file
|
||||
# until 2026-07-20. hardhat.config.coverage.js writes its instrumented cache and
|
||||
# artifacts to `AERE_COVERAGE_OUT || "./cov-out"`, so running the documented
|
||||
# coverage command in a clean clone produced roughly 167 MB of untracked build
|
||||
# output that git would cheerfully offer to commit. The two entries below it
|
||||
# (`cache-coverage/`, `artifacts-coverage/`) are names the config does not use
|
||||
# and are kept only in case an older revision is checked out.
|
||||
#
|
||||
# Worth noting how this was found: by running the documented command rather than
|
||||
# by reading the file. Instructions that have never been followed are usually
|
||||
# wrong somewhere, and this was the somewhere.
|
||||
cov-out/
|
||||
cache-coverage/
|
||||
artifacts-coverage/
|
||||
coverage/
|
||||
@ -25,3 +38,30 @@ coverage.json
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Secret-bearing file types. This repository holds NO keys by design; these
|
||||
# entries exist so that an accidental `git add -A` cannot change that.
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
*.keystore
|
||||
id_rsa*
|
||||
id_ed25519*
|
||||
*.mnemonic
|
||||
secrets.json
|
||||
|
||||
# Compiled Python. Added 2026-07-20 after a committed __pycache__ entry in a
|
||||
# sibling repository was found to embed an absolute Windows build path
|
||||
# containing the operator's account name. Text-based secret scanners skip
|
||||
# compiled binaries, so the scanner passed and the leak shipped. Ignoring the
|
||||
# artifact is the cheap half of the fix; the other half is not committing
|
||||
# build output at all.
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
|
||||
# Python environments
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
18
CITATIONS-UNRESOLVED.md
Normal file
18
CITATIONS-UNRESOLVED.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Citations in this repository that you cannot open
|
||||
|
||||
This file is generated by the legaturi-repara.cjs script in the Aere working tree and
|
||||
enforced by legaturi.cjs. It is the complete list, measured on 2026-08-11, of every
|
||||
path cited in this repository that does not resolve to a published file.
|
||||
|
||||
A citation is a promise that a claim is checkable. Where the target is not published,
|
||||
the claim is traceable but **not independently verifiable by you**, and it should be
|
||||
read as our assertion. Nothing was deleted to produce this list: every path below is
|
||||
still cited in the text exactly as our authors wrote it.
|
||||
|
||||
Paths that DO resolve are written relative to a side-by-side checkout of the Aere
|
||||
repositories, so they begin with a repository name, for example
|
||||
`aere-contracts/contracts/pqc/AereCryptoRegistry.sol`.
|
||||
|
||||
Unresolvable distinct paths in this repository: **1**.
|
||||
|
||||
- `aerenew/scripts/deschise/flaguri-verify.cjs` cited in: aere-contracts/VERIFY-POLICY.md
|
||||
11
LICENSE
11
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 AERE Network
|
||||
Copyright (c) 2026 AERE Network
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -18,4 +18,11 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
PER-FILE EXCEPTIONS
|
||||
|
||||
The grant above is the default for this repository. Individual files that carry
|
||||
their own SPDX-License-Identifier header are governed by that header, not by the
|
||||
grant above. Every exception in this repository is listed in LICENSING.md.
|
||||
|
||||
72
LICENSING.md
Normal file
72
LICENSING.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Licensing
|
||||
|
||||
Default for this repository: **MIT**, `Copyright (c) 2026 AERE Network`. See `LICENSE`.
|
||||
|
||||
That default does not cover every file. Some files here are third-party code vendored in
|
||||
unmodified, and they keep the licence their authors gave them. A few of our own files carry a
|
||||
copyleft header. Where a file has its own `SPDX-License-Identifier`, **that header governs, not
|
||||
the repository default.**
|
||||
|
||||
This page lists every exception. It was produced by reading the `SPDX-License-Identifier` line of
|
||||
every file in the tree, not from memory.
|
||||
|
||||
## Counts, as measured
|
||||
|
||||
| SPDX identifier | Files |
|
||||
| --- | ---: |
|
||||
| `MIT` | 273 |
|
||||
| `Apache-2.0` | 7 |
|
||||
| `LGPL-3.0-only` | 4 |
|
||||
| `GPL-3.0` | 2 |
|
||||
| `MIT OR Apache-2.0` | 1 |
|
||||
|
||||
## The exceptions, by path
|
||||
|
||||
### `contracts/zkverify/risczero/` — RISC Zero verifier, GPL-3.0 and Apache-2.0
|
||||
|
||||
Vendored from RISC Zero. Two of these files are copyleft. If you redistribute them, or a work
|
||||
derived from them, GPL-3.0 applies to that redistribution regardless of anything this repository
|
||||
says elsewhere.
|
||||
|
||||
| File | Licence | Copyright holder |
|
||||
| --- | --- | --- |
|
||||
| `Groth16Verifier.sol` | `GPL-3.0` | 0KIMS association, generated by snarkJS |
|
||||
| `RiscZeroGroth16Verifier.sol` | `GPL-3.0` | RISC Zero, Inc. |
|
||||
| `ControlID.sol` | `Apache-2.0` | RISC Zero, Inc. |
|
||||
| `IRiscZeroSelectable.sol` | `Apache-2.0` | RISC Zero, Inc. |
|
||||
| `IRiscZeroVerifier.sol` | `Apache-2.0` | RISC Zero, Inc. |
|
||||
| `RiscZeroVerifierRouter.sol` | `Apache-2.0` | RISC Zero, Inc. |
|
||||
| `StructHash.sol` | `Apache-2.0` | RISC Zero, Inc. |
|
||||
| `Util.sol` | `Apache-2.0` | RISC Zero, Inc. |
|
||||
|
||||
### `contracts/zkverify/v6/PlonkVerifier.sol` — Apache-2.0
|
||||
|
||||
Generated by gnark. `Copyright 2023 Consensys Software Inc.`
|
||||
|
||||
### `contracts/stablecoins/IHypMailbox.sol` — MIT OR Apache-2.0
|
||||
|
||||
Hyperlane interface, dual licensed by its author. Either arm may be chosen.
|
||||
|
||||
### `contracts/mpc/` — LGPL-3.0-only
|
||||
|
||||
| File |
|
||||
| --- |
|
||||
| `AerePQCThreshold.sol` |
|
||||
| `AereThresholdAccount.sol` |
|
||||
| `AereThresholdAccountFactory.sol` |
|
||||
| `EchoTarget.sol` |
|
||||
|
||||
These four are AERE Network's own work, but they carry an `LGPL-3.0-only` header. Until the
|
||||
provenance of that header is settled in writing, treat these files as LGPL-3.0-only, which is what
|
||||
their header says. They are not offered under the repository's MIT default.
|
||||
|
||||
## Why the exceptions are not simply relicensed
|
||||
|
||||
Two reasons, and they point in opposite directions.
|
||||
|
||||
The vendored files are other people's copyright. We have no power to relicense them, and a
|
||||
repository-level notice claiming otherwise would be false rather than generous.
|
||||
|
||||
The `contracts/mpc/` files are our copyright, so we could relicense them, but the direction of that
|
||||
change matters. Rewriting a copyleft header to a permissive one is only safe if the file is not
|
||||
derived from copyleft code. That has not been established for these four, so the header stands.
|
||||
67
README.md
67
README.md
@ -48,24 +48,81 @@ code in this repository.
|
||||
|
||||
```bash
|
||||
npm ci # installs the exact locked dependency tree
|
||||
npx hardhat compile
|
||||
npx hardhat test
|
||||
npm run compile
|
||||
npm test
|
||||
```
|
||||
|
||||
`npm run` lists every entry point this repository offers. Until 2026-08-03 that
|
||||
list was empty, because `package.json` carried no `scripts` key at all: `npm run`
|
||||
answered with zero lines and exit code 0, which reads as "this project has nothing
|
||||
to run" beside 155 test files. The available entries are now:
|
||||
|
||||
| Command | What it runs |
|
||||
| --- | --- |
|
||||
| `npm run compile` | `hardhat compile` |
|
||||
| `npm test` | `hardhat test`, the whole suite |
|
||||
| `npm run test:pqc` | the Falcon, SPHINCS+ and ML-DSA known-answer tests only |
|
||||
| `npm run coverage` | coverage over the economic-core cluster, the one scope we measure |
|
||||
| `npm run clean` | `hardhat clean` |
|
||||
|
||||
These are entry points, not claims of a green result. Read the next paragraphs
|
||||
before quoting anything from them.
|
||||
|
||||
**MEASURED 2026-08-03 against this published tree, on Node v24.14.1 and npm
|
||||
11.11.0: `npm test` aborts before a single test runs.** `test/shutter-mempool-v2.test.js`
|
||||
and `test/shutter-mempool-v3.test.js` both `require('../scripts/shutter-crypto')`,
|
||||
and `scripts/` is one of the directories deliberately excluded from publication, so
|
||||
Mocha fails to load and the run ends with `MODULE_NOT_FOUND`. Placing that one helper
|
||||
back into `scripts/` makes the suite load and start executing normally, which is how
|
||||
the cause was confirmed rather than guessed. `npm run test:pqc` is unaffected and
|
||||
passed 101 tests in 32 seconds on the same tree. Until the helper is either published
|
||||
or the two suites stop depending on it, treat the full-suite result from this
|
||||
repository as NOT MEASURED, not as passing and not as failing.
|
||||
|
||||
Use `npm ci`, not `npm install`. `ci` installs the exact tree recorded in
|
||||
`package-lock.json` and fails if the lockfile and `package.json` disagree, so
|
||||
you are running against the same dependency versions we measured. `install` is
|
||||
free to resolve newer versions and will quietly give you a different tree.
|
||||
|
||||
Step-by-step instructions with the expected output at each step, the measured
|
||||
pass and coverage numbers, and the required tool versions are in
|
||||
`REPRODUCE.md`. The same three commands run on every push through
|
||||
`.github/workflows/ci.yml`.
|
||||
pass and coverage numbers, and the required tool versions are in **`REPRODUCE.md`
|
||||
in the `aere-docs` repository**, not in this one. An earlier version of this
|
||||
paragraph referred to `REPRODUCE.md` as though it sat beside this README, which
|
||||
would have sent a first-time reader looking for a file that is not here.
|
||||
|
||||
**Read that document before quoting any number from this repository**, because
|
||||
two of the four CI jobs are currently RED and it says so explicitly: the full
|
||||
test suite does not complete on a 16 GB machine, and the whole-repository
|
||||
coverage figure is NOT MEASURED. Note that the first of those two reasons is
|
||||
the one observed on our internal tree, which still has `scripts/`. On this
|
||||
published tree the full suite fails earlier and for the different reason given
|
||||
above, so memory is not the explanation a reader here should accept. Any "99.8% coverage" figure ever associated
|
||||
with this project was never produced by a coverage tool.
|
||||
|
||||
The same three commands run on every push through the CI pipeline, which is a
|
||||
pair of files:
|
||||
|
||||
- `.gitea/workflows/ci.yml` runs on git.aere.network, the canonical host.
|
||||
- `.github/workflows/ci.yml` runs on GitHub mirrors.
|
||||
|
||||
Gitea Actions scans both directories and prefers `.gitea/`; GitHub scans only
|
||||
`.github/`. The two files are identical except for the `upload-artifact`
|
||||
version, and each file's header explains why they are split and records which
|
||||
jobs are currently passing and failing.
|
||||
|
||||
To reproduce against the live chain, point `AERE_RPC_URL` at the public RPC
|
||||
(`https://rpc.aere.network`, chain ID 2800) and compare compiled bytecode to the on-chain code.
|
||||
The full reproducibility manifest lives in the `aere-docs` repository (`REPRODUCE.md`).
|
||||
|
||||
## Following citations in this repository
|
||||
|
||||
Paths cited in this repository are written relative to the document that cites them, or relative to
|
||||
a side-by-side checkout of the published Aere repositories, in which case they begin with a
|
||||
repository name such as `aere-docs/REPRODUCE.md`. MEASURED on 2026-08-03, every cited path in this
|
||||
repository resolves to a published file: `CITATIONS-UNRESOLVED.md` is the enforced list of the ones
|
||||
that do not, and it currently has zero entries. It is generated by a check, not maintained by hand,
|
||||
so it cannot silently fall behind.
|
||||
|
||||
## What is deliberately not here
|
||||
|
||||
Deployment scripts, deploy keys, `.env` files, infrastructure configuration, and business or
|
||||
|
||||
117
VERIFY-POLICY.md
Normal file
117
VERIFY-POLICY.md
Normal file
@ -0,0 +1,117 @@
|
||||
# The `[VERIFY]` marker: what it means in this repository
|
||||
|
||||
**Status:** normative for every Aere Network repository published on `git.aere.network`.
|
||||
**Written:** 2026-08-03. Supersedes the earlier wording that described these markers as items
|
||||
"for a human to confirm before publication".
|
||||
|
||||
---
|
||||
|
||||
## The rule, in one paragraph
|
||||
|
||||
A `[VERIFY]` or `[VERIFY: ...]` marker is a **permanent, deliberate disclosure that one named fact
|
||||
is NOT MEASURED**. It is published exactly as it stands. It does not block publication. It is closed
|
||||
in exactly one way, by a measurement that settles the named fact, and the closing measurement is
|
||||
written down next to it. **A marker is never closed by deleting it**, and it is never closed by
|
||||
softening the sentence around it.
|
||||
|
||||
## Why the earlier wording was wrong, and how
|
||||
|
||||
Three documents in this bundle used to say that these markers were flags "for a human to confirm
|
||||
before publication" or "before publishing":
|
||||
|
||||
- `AERE-EIP-COMPATIBILITY-MATRIX.md`, the "How to read this" list and the open-flags heading
|
||||
- `AERE-NIST-VALIDATION-STATUS.md`, the Appendix A heading and one marker body
|
||||
- `AERE-PROTOCOL-SPECIFICATION.md`, the honesty conventions list and the Section 11 register heading
|
||||
|
||||
That sentence was not true. These repositories were published on 2026-07-20 with **263** exact
|
||||
`[VERIFY]` markers still open across **83** files (measured 2026-08-03 against the published trees,
|
||||
not against a working copy). A document that says its unknowns will be resolved before publication,
|
||||
and is then published with the unknowns in place, asserts more than was done. That is the same class
|
||||
of defect the markers exist to prevent, one level up: **the honesty convention itself was making an
|
||||
unmeasured claim.**
|
||||
|
||||
The repair was to make the text match the practice, not to make the practice match the text. Deleting
|
||||
243 disclosures so that a grep would come back empty would have removed real information from readers
|
||||
and replaced an honest document with a quiet one.
|
||||
|
||||
## Why publishing open unknowns is the right choice here
|
||||
|
||||
This is the same reasoning already recorded in `KNOWN-ISSUES.md` in `aere-contracts`, where our own
|
||||
test suite's findings are published deliberately, with status attached. Publishing a finding is
|
||||
useful. Publishing it with no status was the oversight, and the fix was to add status, not to remove
|
||||
the finding.
|
||||
|
||||
The prior art we followed is the Rust RFC process, where every RFC carries a mandatory
|
||||
"Unresolved questions" section, so the unknowns of a design are an enumerated, permanent part of the
|
||||
published document rather than something that disappears at publication time
|
||||
(`https://github.com/rust-lang/rfcs/blob/master/0000-template.md`).
|
||||
|
||||
The prior art we deliberately did **not** follow is the common docs-CI habit of failing a build on
|
||||
the literal string `TODO` (for example a Vale `existence` rule at `level: error`,
|
||||
`https://docs.vale.sh/topics/styles`). That rule is right for a marker that means "I have not
|
||||
finished writing this sentence". It is wrong for a marker that means "this fact about a live chain
|
||||
has not been measured", because the only thing it can force is deletion, and deletion here is a lie.
|
||||
It is worth noting that GitLab, which runs one of the more thorough public documentation pipelines
|
||||
(Vale, markdownlint, link checking, Mermaid validation), does **not** gate on placeholder markers at
|
||||
all (`https://docs.gitlab.com/development/documentation/testing/`).
|
||||
|
||||
## What is actually enforced
|
||||
|
||||
The guard is `aerenew/scripts/deschise/flaguri-verify.cjs` in the private working repository. It is
|
||||
not a marker ban. It checks four things, and it distinguishes "measured and wrong" from
|
||||
"could not measure":
|
||||
|
||||
1. **No document may promise that markers are closed before publication.** The forbidden phrasings
|
||||
are matched across line breaks, because a sentence split by a line wrap is the same sentence.
|
||||
2. **Every published repository that carries at least one marker must carry this file at its root.**
|
||||
A marker whose meaning is only defined in some other repository is undefined for the reader who
|
||||
found it.
|
||||
3. **The count is tracked, never asserted.** Markers are counted in both forms, `[VERIFY]` and
|
||||
`[VERIFY: ...]`, and compared against the recorded snapshot below. Silent drift fails.
|
||||
4. **The published surface is measured over the network, not from disk.** A working copy is not
|
||||
evidence about what the world can read. If the network cannot be reached, that half of the
|
||||
measurement reports NOT MEASURED and never reports green.
|
||||
|
||||
## Recorded snapshot
|
||||
|
||||
This file is excluded from every count below and from the guard's own counting, because it defines
|
||||
the marker rather than carrying a disclosure. Counting the definition would make the number grow by
|
||||
writing the rule down.
|
||||
|
||||
Working tree of `aerenew/publish-bundle`, measured 2026-08-03:
|
||||
|
||||
| Repository | `[VERIFY]` exact | `[VERIFY` any form | Files carrying a marker |
|
||||
|---|---|---|---|
|
||||
| `aere-contracts` | 8 | 16 | 10 |
|
||||
| `aere-docs` | 177 | 226 | 46 |
|
||||
| `aere-node` | 0 | 0 | 0 |
|
||||
| `aere-research` | 60 | 66 | 18 |
|
||||
| **total** | **245** | **308** | **74** |
|
||||
|
||||
Published trees on `git.aere.network`, cloned and measured 2026-08-03
|
||||
(`aere-docs` at `58780bd`, `aere-contracts` at `c0fcf3d`, `aere-node` at `48416df`,
|
||||
`aere-research` at `37f55e4`, all four commits dated 2026-07-20):
|
||||
|
||||
| Repository | `[VERIFY]` exact | `[VERIFY` any form | Files carrying a marker |
|
||||
|---|---|---|---|
|
||||
| `aere-contracts` | 8 | 16 | 10 |
|
||||
| `aere-docs` | 171 | 224 | 46 |
|
||||
| `aere-node` | 0 | 0 | 0 |
|
||||
| `aere-research` | 84 | 92 | 27 |
|
||||
| **total** | **263** | **332** | **83** |
|
||||
|
||||
The two tables do not agree, and that disagreement is itself a finding rather than an error in the
|
||||
counting. The working copy and the published repositories have drifted apart in **both** directions
|
||||
since 2026-07-20: the working copy holds 21 files that were never published (including
|
||||
`KNOWN-ISSUES.md`, `LICENSING.md`, `CITATIONS-UNRESOLVED.md`, `RUN-A-NODE.md`, `genesis.json` and
|
||||
`bootnodes.txt`), and the published `aere-research` holds 33 files under `formal-consensus/` that no
|
||||
longer exist in the working copy. Any future publication has to reconcile that first, because a
|
||||
naive re-stage and force-push would silently delete 33 files that are public today.
|
||||
|
||||
## For a reader who is not us
|
||||
|
||||
If you found a `[VERIFY]` marker in one of these documents: it is there on purpose. It marks the
|
||||
exact boundary of what we measured. Everything around it is intended to be literally true, and the
|
||||
marker tells you which specific claim we could not stand behind on the date the document was written.
|
||||
If a marker matters to you, the fastest way to close it is usually to measure it yourself against
|
||||
the public endpoint, and we would rather you did that than trust us.
|
||||
@ -10,10 +10,10 @@ import "./paymaster/PaymasterBase.sol";
|
||||
* for UserOps calling contracts on the dApp's whitelist, and (optionally)
|
||||
* only for senders on the dApp's allowlist.
|
||||
*
|
||||
* Use case: Bank28 deploys their own AereAppPaymaster, funds it with X AERE
|
||||
* per month from their CAC budget, whitelists Bank28's contracts as sponsorship
|
||||
* targets, and optionally allowlists the senders they've authenticated through
|
||||
* the Bank28 app. Foundation contributes zero.
|
||||
* Use case: a consumer wallet application deploys its own AereAppPaymaster,
|
||||
* funds it from its own treasury, whitelists its contracts as sponsorship
|
||||
* targets, and optionally allowlists the senders it has authenticated in its
|
||||
* app. Foundation contributes zero.
|
||||
*
|
||||
* Same pattern works for any dApp: NFT marketplaces, games, DeFi apps.
|
||||
*/
|
||||
|
||||
@ -3,7 +3,7 @@ pragma solidity ^0.8.19;
|
||||
|
||||
/**
|
||||
* @title AereCardEscrow
|
||||
* @notice Pre-auth + capture rail for Bank28 (and future card programs) on AERE.
|
||||
* @notice Pre-auth + capture rail for card programs on AERE.
|
||||
* Settlement node calls preAuth() to lock user funds via EIP-2612 permit
|
||||
* (~3s, 1 block on AERE QBFT). When the card auth resolves, the node
|
||||
* calls capture() with the final amount, which transfers to the
|
||||
@ -40,7 +40,7 @@ contract AereCardEscrow {
|
||||
address asset;
|
||||
uint128 amount; // amount on hold (residual after partial captures)
|
||||
uint64 expiresAt;
|
||||
uint64 programId; // 1 = Bank28, future programs allocated by owner
|
||||
uint64 programId; // program ids allocated by owner
|
||||
Status status;
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ contract AereCardEscrow {
|
||||
// ── Programs ───────────────────────────────────────────────────
|
||||
struct Program {
|
||||
address settlementNode; // authorized to call preAuth/capture for this program
|
||||
address feeRecipient; // program's own fee bucket (Bank28 treasury)
|
||||
address feeRecipient; // program's own fee bucket
|
||||
uint16 programFeeBps; // program-charged bps on capture (max 1000)
|
||||
bool active;
|
||||
}
|
||||
|
||||
@ -11,7 +11,8 @@ interface IBurnVault {
|
||||
/**
|
||||
* @title AereCoinbaseSplitter
|
||||
* @notice Routes validator coinbase rewards through an atomic split:
|
||||
* burnBps to AereFeeBurnVault (default 3750 = 37.5%, matching whitepaper §3.3),
|
||||
* burnBps to AereFeeBurnVault (default 3750 = 37.5% of the VALIDATOR
|
||||
* COINBASE REWARD, not of transaction fees and not a base-fee burn),
|
||||
* remainder back to the validator's address.
|
||||
*
|
||||
* Validators (or their forwarder daemons) call `splitAndDistribute()` with
|
||||
@ -20,13 +21,21 @@ interface IBurnVault {
|
||||
* 2. Sends the remainder back to the validator's address.
|
||||
* 3. Emits Burned + Distributed events with cumulative totals.
|
||||
*
|
||||
* Anyone can call this contract — there is no allowlist of "who can burn."
|
||||
* Anyone can call this contract. There is no allowlist of "who can burn."
|
||||
* The splitter is owner-configurable only for the burnBps parameter and
|
||||
* the burn-vault address; it cannot hold or steal funds.
|
||||
*
|
||||
* Cumulative on-chain accounting allows /network-status to display a live
|
||||
* burn rate, and any third-party explorer or analyst to verify the
|
||||
* whitepaper §3.3 deflation claim in O(1) reads.
|
||||
* burn rate, and any third-party explorer or analyst to verify the realized
|
||||
* burn in O(1) reads.
|
||||
*
|
||||
* MEASURED REALITY, and read this before quoting the 37.5% anywhere: the
|
||||
* burn is a percentage of validator coinbase revenue, and that revenue is
|
||||
* currently ZERO on chain 2800. AereFeeBurnVault holds approximately 0.137
|
||||
* AERE against a 2.8 billion fixed supply (eth_getBalance at block
|
||||
* 10,571,949). 37.5% is a CONDITIONAL RATE on future revenue, not a
|
||||
* statement that tokens are being destroyed today. AERE is NOT deflationary
|
||||
* today, and this contract should never be cited as evidence that it is.
|
||||
*/
|
||||
contract AereCoinbaseSplitter is Ownable, ReentrancyGuard {
|
||||
/// AereFeeBurnVault — where the burned portion goes.
|
||||
|
||||
@ -27,14 +27,25 @@ interface IWAERE {
|
||||
*
|
||||
* Three buckets, all in basis points, owner-configurable within hard
|
||||
* caps:
|
||||
* burnBps — direct burn at AereFeeBurnVault (default 3750 = 37.5%)
|
||||
* sinkBps — routed through AereSink (default 1500 = 15.0%)
|
||||
* rebateBps — back to validator (default 4750 = 47.5%, derived)
|
||||
* burnBps = direct burn at AereFeeBurnVault (default 3750 = 37.5%)
|
||||
* sinkBps = routed through AereSink (default 1500 = 15.0%)
|
||||
* rebateBps = back to validator (default 4750 = 47.5%, derived)
|
||||
*
|
||||
* The 3-way default lifts the protocol-level deflation rate from 37.5%
|
||||
* to ~52.5% net (burn bucket plus most of the sink — see saere_architecture
|
||||
* for AereSink's internal 15/40/45 split which makes 55% of sink output
|
||||
* a burn).
|
||||
* All three are shares of the VALIDATOR COINBASE REWARD. None of them
|
||||
* is a share of transaction fees and none is a base-fee burn.
|
||||
*
|
||||
* The 3-way default raises the protocol-directed share of a validator
|
||||
* reward from 37.5% to 52.5% (burn bucket plus the sink bucket). Of
|
||||
* that 52.5%, the portion actually destroyed is 45.75%, because
|
||||
* AereSink's immutable internal 15/40/45 split sends 45% of its input
|
||||
* to staker yield rather than to a burn. 45.75% is the honest number
|
||||
* for token destruction; 52.5% counts the whole sink slice as
|
||||
* protocol-directed. Both are CONDITIONAL RATES on a validator reward.
|
||||
*
|
||||
* MEASURED: validator coinbase revenue on chain 2800 is currently ZERO,
|
||||
* so every rate above currently applies to zero. AereFeeBurnVault holds
|
||||
* approximately 0.137 AERE against a 2.8 billion fixed supply
|
||||
* (eth_getBalance at block 10,571,949). AERE is NOT deflationary today.
|
||||
*
|
||||
* @dev Owner can adjust bps within these limits:
|
||||
* - burnBps ∈ [0, 5000]
|
||||
|
||||
@ -3,23 +3,67 @@ pragma solidity ^0.8.19;
|
||||
|
||||
/**
|
||||
* @title AereFeeBurnVault
|
||||
* @notice Permanent, on-chain burn destination for protocol fees.
|
||||
* @notice Permanent, on-chain burn destination for protocol revenue. Read the
|
||||
* two sections below before quoting any burn number from this contract:
|
||||
* what is burned is not what the whitepaper wording suggests, and the
|
||||
* amount burned to date is effectively zero.
|
||||
*
|
||||
* Whitepaper §3.3 promises that "up to 37.5% of all transaction fees are
|
||||
* permanently obliterated." This contract is the burn endpoint that makes
|
||||
* that claim measurable and publicly auditable.
|
||||
* WHAT IS BURNED, STATED PRECISELY. The Aere burn is a cut of the VALIDATOR
|
||||
* (coinbase) block reward. It is NOT a base-fee burn and NOT a share of
|
||||
* transaction fees. The default rate is 37.5% (burnBps = 3750) and it is hard
|
||||
* capped at 50%: AereCoinbaseSplitterV2 enforces the cap with the named
|
||||
* constant MAX_BURN_BPS = 5000, and AereCoinbaseSplitter (V1) enforces the same
|
||||
* ceiling with an unnamed `require(_bps <= 5000)` inside `setBurnBps`. Whitepaper
|
||||
* section 3.3 words the burn as a share of all transaction fees; that wording
|
||||
* predates the implemented mechanism and is superseded by this paragraph, which
|
||||
* matches AERE-PROTOCOL-SPECIFICATION.md, AERE-EIP-COMPATIBILITY-MATRIX.md and
|
||||
* AERE-ENGINEERING-SECURITY-SPEC.md, all three of which state in terms that the
|
||||
* Aere burn must not be described as a base-fee burn.
|
||||
*
|
||||
* REALIZED BURN TO DATE, MEASURED. Chain 2800, deployment
|
||||
* 0x696afDF4f814e6Fd6aa45CE14C498ed9375fB2c6, measured at block 11,967,981 on
|
||||
* 2026-08-02 with `eth_getProof` (not with a nonce read, which can return a
|
||||
* false zero outside the public RPC state window):
|
||||
*
|
||||
* balance = 137352594046167719 wei = 0.13735 AERE
|
||||
* totalBurnedAERE() = 137352594046167719 wei (identical)
|
||||
* totalSentToZero() = 0 (`sweepToZero` has NEVER run)
|
||||
*
|
||||
* Against a fixed supply of 2,800,000,000 AERE that is about 0.0000000049
|
||||
* percent of supply. The reason is arithmetic, not malfunction: validator
|
||||
* coinbase revenue on this chain is currently zero, and a percentage of zero is
|
||||
* zero. Every burn percentage stated anywhere in this repository is therefore a
|
||||
* CONDITIONAL RATE that would apply to future validator revenue, not a
|
||||
* description of tokens destroyed to date. AERE is NOT deflationary today.
|
||||
* Anyone can recheck both numbers with one `eth_getProof` and one `eth_call`
|
||||
* against the address above, and is encouraged to.
|
||||
*
|
||||
* BYTECODE IDENTITY AFTER THIS CORRECTION, MEASURED. The two sections above are
|
||||
* a comment-only change, and a comment-only change still moves the appended CBOR
|
||||
* metadata hash, because that hash covers the source text. Recompiled with the
|
||||
* profile the deployed contract was built under (solc 0.8.19, optimizer enabled
|
||||
* with runs = 200, viaIR = true) and compared byte for byte against the live code
|
||||
* at 0x696afDF4f814e6Fd6aa45CE14C498ed9375fB2c6, this file yields the same 1313
|
||||
* bytes with ZERO differing bytes outside the metadata trailer. Every difference
|
||||
* falls inside that trailer, which is the fingerprint of the source text itself.
|
||||
* The executable contract is unchanged; only its fingerprint moved. In Sourcify
|
||||
* terms the older text was a full (exact) match and this text is a partial match.
|
||||
* The previous, misleading text is what produced the deployed fingerprint; that is
|
||||
* a reason to record the fact here, not a reason to keep publishing a false claim.
|
||||
*
|
||||
* Design:
|
||||
* - Stateless sink: anyone can send native AERE here via `burn()`, or any
|
||||
* ERC-20 via `burnToken()`. The contract has NO withdraw function and
|
||||
* NO admin escape hatch — funds that enter are permanently removed
|
||||
* NO admin escape hatch, so funds that enter are permanently removed
|
||||
* from circulation.
|
||||
* - Counters: tracks `totalBurnedAERE` and per-token `totalBurnedToken[token]`
|
||||
* so the chain's deflation can be queried in a single read.
|
||||
* - Sources: protocol contracts that earn fees (AereSwapRouter, the NFT
|
||||
* so realized burn can be queried in a single read. `totalBurnedAERE` is a
|
||||
* cumulative arrival counter, not a supply-destruction figure; see the
|
||||
* `sweepToZero` note below for the difference.
|
||||
* - Sources: protocol contracts that earn revenue (AereSwapRouter, the NFT
|
||||
* marketplace, future card-fee router, etc.) can route a configurable
|
||||
* share to this address — but the vault doesn't care WHO sends; any
|
||||
* value arriving here is burned.
|
||||
* share to this address. The vault does not care WHO sends; any value
|
||||
* arriving here is burned.
|
||||
*
|
||||
* This contract holds no privilege over any other system. Ownable is not
|
||||
* needed because there are no admin operations.
|
||||
@ -34,7 +78,7 @@ interface IERC20 {
|
||||
contract AereFeeBurnVault {
|
||||
/// Total native AERE that has flowed into this contract.
|
||||
/// Equal to address(this).balance EXCEPT after the eventual
|
||||
/// "send to zero-address" sweep — see note below.
|
||||
/// "send to zero-address" sweep. See the `sweepToZero` note below.
|
||||
uint256 public totalBurnedAERE;
|
||||
|
||||
/// Cumulative native AERE actually forwarded to address(0).
|
||||
@ -73,7 +117,11 @@ contract AereFeeBurnVault {
|
||||
/// Optional: forward accumulated native AERE to address(0).
|
||||
/// QBFT Besu permits sending to address(0); the AERE is then unreachable.
|
||||
/// Anyone can call this; it has no parameters and no admin gate. This is
|
||||
/// the "obliteration" step — until called, AERE sits on this contract.
|
||||
/// the final removal step. Until it is called, AERE sits on this contract,
|
||||
/// which is unreachable for any other reason but is still a live account.
|
||||
/// MEASURED at block 11,967,981 on 2026-08-02: `totalSentToZero` is 0, so
|
||||
/// this function has NEVER been called on chain 2800 and no AERE has been
|
||||
/// sent to address(0) by this vault.
|
||||
function sweepToZero() external {
|
||||
uint256 bal = address(this).balance;
|
||||
require(bal > 0, "BurnVault: nothing to sweep");
|
||||
|
||||
@ -12,7 +12,7 @@ import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
||||
*
|
||||
* - Developer calls register(contractAddr, payoutAddr) and receives an
|
||||
* ERC-721 NFT representing the right to collect fees from contractAddr.
|
||||
* - An off-chain distributor (running off chain) parses recent blocks,
|
||||
* - An off-chain distributor (Foundation-operated) parses recent blocks,
|
||||
* attributes gas per top-level contract, and calls distribute() with
|
||||
* per-tokenId amounts.
|
||||
* - NFT owner calls claim(tokenId) to withdraw accumulated AERE.
|
||||
@ -22,12 +22,19 @@ import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
||||
* Plus: a fixed `treasuryBps` slice that always flows to AERE Foundation
|
||||
* (separate from per-contract dev-share). Default 500 = 5%.
|
||||
*
|
||||
* Fee distribution per gas fee paid (set by Foundation governance):
|
||||
* burnBps (Tier 1.8): 3750 = 37.5% (permanent burn)
|
||||
* Intended allocation of VALIDATOR COINBASE REVENUE attributable to a
|
||||
* registered contract's gas usage (set by Foundation governance). These are
|
||||
* shares of the validator's coinbase reward, NOT a base-fee burn and NOT a
|
||||
* protocol-level cut of transaction fees:
|
||||
* burnBps (Tier 1.8): 3750 = 37.5% (permanent burn, capped at 5000)
|
||||
* treasuryBps (this tier): 500 = 5% (always to Foundation)
|
||||
* devShareBps (this tier): 2000 = 20% (to registered NFT owner, else to validator)
|
||||
* validatorBps (residual): 3750 = 37.5% (to block validator)
|
||||
*
|
||||
* MEASURED: validator coinbase revenue on chain 2800 is currently zero, so
|
||||
* these shares currently distribute nothing and no fee stream has accrued.
|
||||
* The percentages are conditional rates on future revenue.
|
||||
*
|
||||
* Foundation registers its own foundational contracts (AereSwapRouter,
|
||||
* AereSettlement, AereMessenger, AereNFTMarketplace, etc.) so their dev-share
|
||||
* flows back to Foundation. External devs register their own; their NFT owner
|
||||
|
||||
@ -198,7 +198,7 @@ contract AereFeeMonetizationV2 is ERC721, Ownable, ReentrancyGuard {
|
||||
|
||||
/// @notice Convenience: Foundation registers its OWN protocol contracts in
|
||||
/// bulk. This is an onlyOwner ADMIN bootstrap (the FeeMonetization
|
||||
/// owner is the Foundation multisig) — it is NOT the permissionless
|
||||
/// owner is the Foundation account, a single-key EOA today, not a multisig) — it is NOT the permissionless
|
||||
/// register() path and is NOT a squat vector: only the Foundation can
|
||||
/// call it, and only for the contracts it operates.
|
||||
function registerBatch(address[] calldata contractAddrs, address payoutAddr)
|
||||
|
||||
@ -6,8 +6,16 @@ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
|
||||
/**
|
||||
* @title AereMiningDistributor
|
||||
* @notice Merkle-claim payout distributor for AireFlow / mining-subscription
|
||||
* epoch rewards.
|
||||
* @notice Generic Merkle-claim payout distributor, originally written for the
|
||||
* AireFlow / mining-subscription epoch payouts.
|
||||
*
|
||||
* @dev Its intended counterparty, AereMiningSubscription, is NOT OFFERED and NOT
|
||||
* DEPLOYED: Aere is a proof-of-authority chain with no mining, no hash rate
|
||||
* and no mining rewards. See the note at the top of
|
||||
* AereMiningSubscription.sol. The Merkle claim mechanism in this file is
|
||||
* generic and makes no claim about the source, size or existence of any
|
||||
* payout; amounts are whatever the Foundation posts in a root and funds.
|
||||
* No return of any kind is guaranteed or implied.
|
||||
*
|
||||
* Workflow each epoch (default = 30 days, matches AereMiningSubscription):
|
||||
*
|
||||
|
||||
@ -6,13 +6,35 @@ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
|
||||
/**
|
||||
* @title AereMiningSubscription (AireFlow)
|
||||
* @notice Cloud mining subscriptions paid in AERE
|
||||
* @dev Whitepaper §5.3 — "Revolutionary Subscription Mining Platform"
|
||||
* Users pay a monthly AERE fee for guaranteed hash rate allocation,
|
||||
* receive proportional block rewards distributed by the foundation.
|
||||
* @notice Subscription accounting contract. Users pay a recurring AERE fee and
|
||||
* accrue a payout computed from owner-configured parameters, settled
|
||||
* from a Foundation-funded pool held by this contract.
|
||||
*
|
||||
* Reference whitepaper §5.3:
|
||||
* AireFlow 15.0 TH/s example: $300/month → ~30 AERE/month rewards (illustrative).
|
||||
* @dev NOT OFFERED, NOT DEPLOYED, AND NOT COHERENT ON THIS CHAIN. Read this
|
||||
* before reading anything else in the file.
|
||||
*
|
||||
* 1. There is no mining on Aere. Chain 2800 is Hyperledger Besu QBFT
|
||||
* proof-of-authority with N=7 Foundation-operated validators. There is
|
||||
* no hash rate, no difficulty and no mining reward. The `hashRateThs`,
|
||||
* `networkHashRateThs` and `difficultyAdjustmentBps` fields below
|
||||
* therefore do not measure anything that exists. They are owner-set
|
||||
* numbers that feed an owner-set payout formula.
|
||||
* 2. Because of (1), the payout is not a share of mining proceeds. It is a
|
||||
* figure the operator chooses, funded by Foundation top-ups and by
|
||||
* subscriber payments held on this contract. Nothing in the code
|
||||
* constrains payouts to any external revenue.
|
||||
* 3. No guarantee of any allocation, payout, rate or value is made by this
|
||||
* contract or by the Aere Foundation. Subscription payments are not an
|
||||
* investment, confer no claim on protocol revenue, and may return
|
||||
* nothing. `claim()` reverts when the pool is empty.
|
||||
* 4. MEASURED, searched across all published repositories: no deployment
|
||||
* address for this contract appears anywhere in the published set. It
|
||||
* is source-only here.
|
||||
*
|
||||
* This contract is retained in the repository as a historical artifact of
|
||||
* an earlier design and is pending a Foundation decision on whether it
|
||||
* should be published at all. It should not be deployed, offered or
|
||||
* referenced as a product in its current form.
|
||||
*/
|
||||
contract AereMiningSubscription is Ownable, ReentrancyGuard {
|
||||
enum Tier { BASIC, PREMIUM, ENTERPRISE, AIREFLOW }
|
||||
@ -117,9 +139,15 @@ contract AereMiningSubscription is Ownable, ReentrancyGuard {
|
||||
|
||||
/**
|
||||
* @notice Pending rewards (in AERE wei) for a subscriber.
|
||||
* @dev Formula (whitepaper §5.3):
|
||||
* R = B × HR × (1 - Dt) × T
|
||||
* @dev Formula (whitepaper section 5.3):
|
||||
* R = B x HR x (1 - Dt) x T
|
||||
* where HR = userHashRate / networkHashRate
|
||||
*
|
||||
* Every input to this formula is owner-configured and none of them
|
||||
* corresponds to a physical quantity on a proof-of-authority chain.
|
||||
* See the NOT OFFERED, NOT DEPLOYED note at the top of this file. The
|
||||
* value returned here is not a guaranteed, promised or expected
|
||||
* return.
|
||||
*/
|
||||
function pendingRewards(address user) public view returns (uint256) {
|
||||
Subscription memory s = subscriptions[user];
|
||||
|
||||
@ -17,7 +17,7 @@ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
* 2. The deployed AereIGP (0x61B48615F490A23945988c92835eF35fdD86E837) has
|
||||
* zero destination gas configs (gasOverhead=0, gasPriceQuoteAere=0 for
|
||||
* every domain), so even its own quoteGasPayment(uint32) returns 0.
|
||||
* 3. Both are owned by the Foundation multisig, so neither could be
|
||||
* 3. Both are owned by the Foundation account (a single-key EOA today, not a multisig), so neither could be
|
||||
* reconfigured by the deployer anyway.
|
||||
*
|
||||
* WHAT THIS IS:
|
||||
|
||||
@ -29,7 +29,7 @@ interface IERC20 {
|
||||
* This contract pattern mirrors Hyperlane Warp Route exactly, so apps built
|
||||
* against Hyperlane's tooling work on AERE with only the contract address change.
|
||||
*
|
||||
* For Bank28: USDC.aere, USDT.aere, WETH.aere, cbBTC.aere are all separate
|
||||
* For a card program: USDC.aere, USDT.aere, WETH.aere, cbBTC.aere are all separate
|
||||
* AereWarpRoute deployments in collateral mode (on the origin chain) and
|
||||
* synthetic mode (on AERE) — totalling 8 deployments for 4 assets across
|
||||
* 2 chain pairs.
|
||||
|
||||
@ -16,7 +16,10 @@ import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
|
||||
* An off-chain ingester running on a Hostinger VPS (#2) reads the
|
||||
* official list nightly, extracts crypto addresses where present,
|
||||
* builds the Merkle tree, and publishes the root via the
|
||||
* Foundation 2-of-3 multisig (Ledger-signed).
|
||||
* Foundation owner account 0x0243A4f4 (measured on chain 2800: an
|
||||
* externally owned account today, not a multisig contract).
|
||||
* Hardware-wallet storage of that key is NOT asserted here;
|
||||
* the chain cannot attest to it.
|
||||
*
|
||||
* Designed as a PUBLIC GOOD. Anyone — institutional integrator,
|
||||
* dApp, EOA — calls `isSanctioned(...)` to gate or screen a
|
||||
|
||||
@ -43,8 +43,8 @@ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
||||
* the signed result to the exact circuit that was computed.
|
||||
*
|
||||
* TEE PATH NOT BUILT. A hardware-attested (SEV-SNP / TDX / SGX) execution
|
||||
* path was intentionally not built: the AERE server is a
|
||||
* cloud guest with no usable trusted-execution attestation. The security
|
||||
* path was intentionally not built: the AERE server is a cloud guest
|
||||
* with no usable trusted-execution attestation. The security
|
||||
* here is pure-software secret-sharing MPC, which needs no trusted
|
||||
* hardware.
|
||||
*/
|
||||
|
||||
@ -245,11 +245,23 @@ contract AereSpokePool is Ownable, ReentrancyGuard, IOriginSettler {
|
||||
|
||||
/// @notice Foundation slashes a solver's bond on validated fraud.
|
||||
/// Phase-1: Foundation arbitration; Phase-2: UMA settlement.
|
||||
/// @dev OWNER-DIRECTED DESTINATION, NOT A PROTOCOL INVARIANT.
|
||||
/// The slashed amount is transferred to `toRecipient` verbatim.
|
||||
/// AereSink is only the DEFAULT, applied when `toRecipient` is
|
||||
/// address(0). The owner may direct any slash to any address,
|
||||
/// including its own. This function therefore does NOT guarantee
|
||||
/// that a slashed bond is ever sunk or burned. Integrators must
|
||||
/// treat the destination as an owner privilege and read the actual
|
||||
/// `dest` from the transaction, not from this comment.
|
||||
/// @param solver solver whose bond is reduced.
|
||||
/// @param amount amount of WAERE removed from that bond.
|
||||
/// @param toRecipient destination of the slashed amount; address(0) selects SINK.
|
||||
function slashSolverBond(address solver, uint256 amount, address toRecipient) external onlyOwner nonReentrant {
|
||||
if (solverBond[solver] < amount) revert InsufficientBond();
|
||||
solverBond[solver] -= amount;
|
||||
totalBond[WAERE_ADDR] -= amount; // BOND-ACCOUNTING FIX: keep the sweep reserve in lockstep
|
||||
// Slash → routes to AereSink so the burn flywheel benefits.
|
||||
// Destination is chosen by the owner. SINK is only the address(0) default,
|
||||
// so nothing here forces the slashed bond towards the sink.
|
||||
address dest = toRecipient == address(0) ? SINK : toRecipient;
|
||||
bool ok = IERC20(WAERE_ADDR).transfer(dest, amount);
|
||||
if (!ok) revert TransferFailed();
|
||||
|
||||
@ -33,7 +33,7 @@ interface IAereLendingMarketForFund {
|
||||
* FOUNDATION CANNOT DIRECT-WITHDRAW. The only way tokens leave this
|
||||
* contract is `coverBadDebt` paying down debt on a REGISTERED market.
|
||||
* However, market registration IS a Foundation-trust operation: a
|
||||
* compromised Foundation multisig could register a malicious
|
||||
* compromised Foundation owner account (an externally owned account today, not a multisig contract) could register a malicious
|
||||
* "market" whose `repayOnBehalfOf` impl drains the approved
|
||||
* tokens. We mitigate this with a 7-day REGISTRATION_DELAY between
|
||||
* `proposeMarket` and `registerMarket` — long enough for community
|
||||
@ -53,7 +53,7 @@ interface IAereLendingMarketForFund {
|
||||
* - AereSink optional 5% slice (Foundation can route a portion
|
||||
* of buyback-and-burn to this fund instead, via a separate
|
||||
* route — out of scope for this contract)
|
||||
* - Protocol revenue dedications (Bank28 fees, etc.)
|
||||
* - Protocol revenue dedications (partner application fees, etc.)
|
||||
*/
|
||||
contract AereInsuranceFund is ReentrancyGuard {
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ pragma solidity 0.8.23;
|
||||
* @title AereThresholdRegistry, an on-chain registry of t-of-n MPC/TSS signing
|
||||
* committees for threshold ECDSA custody (AERE chain 2800)
|
||||
*
|
||||
* @notice The on-chain anchor a non-custodial custody committee (e.g. a Bank28 vault)
|
||||
* @notice The on-chain anchor a non-custodial custody committee (e.g. a partner custody vault)
|
||||
* uses to PUBLISH the composition of a t-of-n threshold-ECDSA signing group and
|
||||
* to let any relying party VERIFY that a submitted signature was produced under
|
||||
* that group's key. A committee is `{ members[], threshold t, groupPubKey }`.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Threshold PQC on AERE — honest maturity assessment
|
||||
# Threshold PQC on AERE, honest maturity assessment
|
||||
|
||||
Scope: can we make the MPC/TSS custody committee **post-quantum**, i.e. produce a *threshold*
|
||||
ML-DSA (Dilithium) / Falcon / SLH-DSA signature where no single party ever holds the whole
|
||||
@ -10,9 +10,9 @@ threshold PQC. It is not claimed to work.**
|
||||
| Layer | Status on AERE today | Threshold (t-of-n) variant |
|
||||
|---|---|---|
|
||||
| ECDSA (secp256k1) group signing | shipped here (`AereThresholdRegistry` + `sdk-js/src/mpc`) | **mature protocols exist** (GG20/CGGMP21, DKLs23); non-reconstructing signer is delegated to an audited lib, not reimplemented |
|
||||
| ML-DSA-44 verify | LIVE precompile `0x0AE3` (single-key only) | **research** — no standard, no audited production lib |
|
||||
| Falcon-512 / 1024 verify | LIVE precompiles `0x0AE1/0x0AE2` (single-key) | **research, hard** — Gaussian sampling resists thresholding |
|
||||
| SLH-DSA-128s verify | LIVE precompile `0x0AE4` (single-key) | **effectively N/A** — stateless hash-based, thresholding is impractical |
|
||||
| ML-DSA-44 verify | LIVE precompile `0x0AE3` (single-key only) | **research**, no standard, no audited production lib |
|
||||
| Falcon-512 / 1024 verify | LIVE precompiles `0x0AE1/0x0AE2` (single-key) | **research, hard**, Gaussian sampling resists thresholding |
|
||||
| SLH-DSA-128s verify | LIVE precompile `0x0AE4` (single-key) | **effectively N/A**, stateless hash-based, thresholding is impractical |
|
||||
|
||||
The five PQC precompiles verify **ordinary, single-signer** PQC signatures. They have nothing
|
||||
to do with the threshold construction and do not make it post-quantum.
|
||||
@ -31,7 +31,7 @@ schemes are harder to thresholdise at the signing step, for scheme-specific reas
|
||||
noise growth from summing shares. Academic constructions exist (e.g. Cozzo–Smart style
|
||||
MPC-Dilithium, and 2023–2024 threshold-Raccoon / threshold lattice-signature papers using
|
||||
Raccoon which was *designed* to be thresholdisable), but:
|
||||
- Threshold-friendly schemes like **Raccoon are NOT ML-DSA** — different scheme, not
|
||||
- Threshold-friendly schemes like **Raccoon are NOT ML-DSA**, different scheme, not
|
||||
NIST-FIPS-204, and not one of AERE's precompiles. A Raccoon threshold signature would need
|
||||
a *new* verifier, not `0x0AE3`.
|
||||
- No audited, maintained production library produces a **FIPS-204 ML-DSA** signature from
|
||||
@ -45,7 +45,7 @@ schemes are harder to thresholdise at the signing step, for scheme-specific reas
|
||||
|
||||
- **SLH-DSA / SPHINCS+ (stateless hash-based).** A signature is a large Merkle/one-time-key
|
||||
authentication path. There is no algebraic structure to share; you would essentially need
|
||||
generic MPC over the whole hash-tree signing circuit — impractical (huge SLH-DSA signatures,
|
||||
generic MPC over the whole hash-tree signing circuit, impractical (huge SLH-DSA signatures,
|
||||
hash circuits in MPC). Treat as not-applicable for thresholding.
|
||||
|
||||
## What IS honestly available for post-quantum custody today
|
||||
@ -57,14 +57,14 @@ Threshold PQC is not the only route to quantum-resistant *custody*. Honest, ship
|
||||
**single-key PQC co-signature** (ML-DSA-44 via `0x0AE3`) from a separate quorum device, so a
|
||||
quantum attacker must break *both* ECDSA *and* ML-DSA. This is real today: it composes
|
||||
`AereThresholdRegistry` with the existing `AerePQCKeyRegistry` / precompiles. It does **not**
|
||||
give you a *threshold* PQC key — the PQC key is single-party — so it trades one property
|
||||
give you a *threshold* PQC key, the PQC key is single-party, so it trades one property
|
||||
(distributed PQC key) for another (defence-in-depth). Label it honestly as hybrid, not
|
||||
threshold-PQC.
|
||||
|
||||
2. **Threshold of independent PQC signers (k-of-n multisig, not TSS).** n parties each hold
|
||||
their **own** ML-DSA key; a policy contract accepts when ≥ t *distinct* PQC signatures
|
||||
verify. This is a genuine on-chain **multisig** (each precompile call is independent) and is
|
||||
quantum-resistant *and* distributed — but it is an m-of-n **multi-signature**, not a single
|
||||
quantum-resistant *and* distributed, but it is an m-of-n **multi-signature**, not a single
|
||||
compact **threshold** signature, and the on-chain cost is t precompile verifications, not
|
||||
one. This is buildable today on AERE and is the most honest "distributed PQC" you can ship
|
||||
now. It is out of scope for this task (which targets threshold ECDSA TSS) but is the
|
||||
@ -73,8 +73,8 @@ Threshold PQC is not the only route to quantum-resistant *custody*. Honest, ship
|
||||
## Recommendation
|
||||
|
||||
- **Now:** ship threshold **ECDSA** custody (this deliverable) with an audited GG20/DKLs signer;
|
||||
for PQC assurance use option (2) — a k-of-n **independent-PQC multisig** using the live
|
||||
precompiles — and/or option (1) hybrid co-signing. Do not market either as "threshold PQC".
|
||||
for PQC assurance use option (2), a k-of-n **independent-PQC multisig** using the live
|
||||
precompiles, and/or option (1) hybrid co-signing. Do not market either as "threshold PQC".
|
||||
- **Watch:** FIPS-204 threshold standardisation, threshold-Raccoon, and MPC-Dilithium research.
|
||||
Revisit true threshold-ML-DSA only when an **audited** library targets the **standardised**
|
||||
scheme and a matching on-chain verifier exists.
|
||||
|
||||
@ -67,6 +67,33 @@ contract AerePyth is IPyth, Ownable, ReentrancyGuard {
|
||||
/// Maximum age (seconds) a price can be without being considered stale.
|
||||
uint256 public validTimePeriod = 60;
|
||||
|
||||
// ─────────────── Update payload framing (v1) ───────────────
|
||||
//
|
||||
// Constatarea D-067. The first version of this contract inferred the length
|
||||
// of the encoded price feed from the total payload length:
|
||||
// head = data.length - ((data.length - 64) / 65) * 65
|
||||
// For a payload of 288 + 65*k bytes that expression collapses to a constant
|
||||
// 93 for EVERY k, so `abi.decode(data[:head])` reverted on every valid
|
||||
// update that could ever be submitted. The write path was dead arithmetic.
|
||||
//
|
||||
// The replacement follows the framing discipline of the production Pyth
|
||||
// receiver (pyth-network/pyth-crosschain, PythAccumulator.sol): the payload
|
||||
// carries its own magic bytes and version, every variable-length segment is
|
||||
// announced by an explicit count, and the parser asserts that it consumed
|
||||
// the buffer EXACTLY. Nothing is inferred from the total length.
|
||||
//
|
||||
// bytes 0..3 magic "AEPU"
|
||||
// byte 4 version, must be 1
|
||||
// byte 5 sigCount, number of 65-byte signatures that follow the feed
|
||||
// bytes 6..293 abi.encode(PythPriceFeed), fixed 288 bytes (nine static words)
|
||||
// bytes 294.. sigCount * 65 signature bytes
|
||||
// total length must equal 294 + 65*sigCount, no trailing bytes tolerated
|
||||
bytes4 internal constant UPDATE_MAGIC = 0x41455055; // "AEPU"
|
||||
uint8 internal constant UPDATE_VERSION = 1;
|
||||
uint256 internal constant FEED_ENCODED_LEN = 288;
|
||||
uint256 internal constant UPDATE_HEADER_LEN = 6;
|
||||
uint256 internal constant SIG_LEN = 65;
|
||||
|
||||
event PriceUpdated(bytes32 indexed id, int64 price, uint64 conf, int32 expo, uint256 publishTime);
|
||||
event PublisherAdded(address indexed publisher);
|
||||
event PublisherRemoved(address indexed publisher);
|
||||
@ -163,17 +190,18 @@ contract AerePyth is IPyth, Ownable, ReentrancyGuard {
|
||||
|
||||
/**
|
||||
* @notice Pyth-style pull-oracle update.
|
||||
* @dev Each `updateData[i]` is the abi-encoded tuple
|
||||
* (priceFeed, signatures)
|
||||
* where signatures is the concatenated 65-byte ECDSA signatures
|
||||
* of the publisher set over keccak256(abi.encode(priceFeed)).
|
||||
* @dev Each `updateData[i]` is a v1 framed payload: magic "AEPU",
|
||||
* version byte, signature count, the 288-byte abi-encoded
|
||||
* PythPriceFeed, then that many 65-byte ECDSA signatures over
|
||||
* `updateDigest(feed)`. The parser never infers a length.
|
||||
*/
|
||||
function updatePriceFeeds(bytes[] calldata updateData) external payable override nonReentrant {
|
||||
uint256 required = updateFeePerUpdate * updateData.length;
|
||||
require(msg.value >= required, "AerePyth: insufficient fee");
|
||||
|
||||
for (uint256 i = 0; i < updateData.length; i++) {
|
||||
_applyUpdate(updateData[i]);
|
||||
(PythPriceFeed memory feed, bytes calldata sigs) = _parseUpdate(updateData[i]);
|
||||
_verifyAndStore(feed, sigs);
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,42 +211,75 @@ contract AerePyth is IPyth, Ownable, ReentrancyGuard {
|
||||
uint64[] calldata publishTimes
|
||||
) external payable override nonReentrant {
|
||||
require(priceIds.length == publishTimes.length, "AerePyth: length mismatch");
|
||||
// The fee is charged here too. The first version of this function took
|
||||
// no fee at all, so once `updateFeePerUpdate` was raised the same work
|
||||
// was free through this door.
|
||||
require(msg.value >= updateFeePerUpdate * updateData.length, "AerePyth: insufficient fee");
|
||||
|
||||
// Determine which updates are actually needed (publishTimes are newer than what we have).
|
||||
for (uint256 i = 0; i < priceIds.length; i++) {
|
||||
if (_prices[priceIds[i]].publishTime < publishTimes[i]) {
|
||||
// Walk updateData looking for the matching id and apply it.
|
||||
// Caller is expected to send only the relevant updates in updateData order.
|
||||
for (uint256 j = 0; j < updateData.length; j++) {
|
||||
PythPriceFeed memory feed = abi.decode(updateData[j][:_payloadLength(updateData[j])], (PythPriceFeed));
|
||||
if (feed.id == priceIds[i]) {
|
||||
_applyUpdate(updateData[j]);
|
||||
break;
|
||||
}
|
||||
bool applied = false;
|
||||
// Each payload is parsed exactly once, then matched against the ids the
|
||||
// caller says it needs. The previous version decoded every payload once
|
||||
// per requested id inside the inner loop.
|
||||
for (uint256 j = 0; j < updateData.length; j++) {
|
||||
(PythPriceFeed memory feed, bytes calldata sigs) = _parseUpdate(updateData[j]);
|
||||
for (uint256 i = 0; i < priceIds.length; i++) {
|
||||
if (feed.id == priceIds[i] && _prices[priceIds[i]].publishTime < publishTimes[i]) {
|
||||
_verifyAndStore(feed, sigs);
|
||||
applied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Same choice the canonical Pyth receiver makes: say so instead of
|
||||
// taking the fee and silently doing nothing.
|
||||
require(applied, "AerePyth: no fresh update");
|
||||
}
|
||||
|
||||
function _payloadLength(bytes calldata data) internal pure returns (uint256) {
|
||||
// Update layout: <PythPriceFeed encoded> + <signatures (each 65 bytes)>
|
||||
// The encoded PythPriceFeed length is fixed (struct with no dynamic arrays).
|
||||
// We compute by stripping signatures from the tail.
|
||||
uint256 sigsTail = ((data.length - 64) / 65) * 65;
|
||||
return data.length - sigsTail;
|
||||
/**
|
||||
* @notice The exact 32-byte hash a publisher must personal_sign for `feed`.
|
||||
* @dev Bound to this chain and this contract, so a publisher signature
|
||||
* cannot be replayed into another deployment, or onto another chain
|
||||
* that shares the same publisher key. The publisher signs THIS with
|
||||
* EIP-191 personal_sign; the contract adds the same prefix before
|
||||
* ecrecover.
|
||||
*/
|
||||
function updateSigningHash(PythPriceFeed calldata feed) external view returns (bytes32) {
|
||||
return _updatePayloadHash(feed);
|
||||
}
|
||||
|
||||
function _applyUpdate(bytes calldata data) internal {
|
||||
// Decode the price feed from the head of the payload, then verify
|
||||
// threshold signatures over the digest of the encoded feed.
|
||||
uint256 headLen = _payloadLength(data);
|
||||
PythPriceFeed memory feed = abi.decode(data[:headLen], (PythPriceFeed));
|
||||
bytes calldata sigs = data[headLen:];
|
||||
require(sigs.length % 65 == 0, "AerePyth: bad sig length");
|
||||
uint256 sigCount = sigs.length / 65;
|
||||
function _updatePayloadHash(PythPriceFeed memory feed) internal view returns (bytes32) {
|
||||
return keccak256(abi.encode(UPDATE_MAGIC, UPDATE_VERSION, block.chainid, address(this), feed));
|
||||
}
|
||||
|
||||
function _updateDigest(PythPriceFeed memory feed) internal view returns (bytes32) {
|
||||
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _updatePayloadHash(feed)));
|
||||
}
|
||||
|
||||
function _parseUpdate(bytes calldata data)
|
||||
internal
|
||||
pure
|
||||
returns (PythPriceFeed memory feed, bytes calldata sigs)
|
||||
{
|
||||
require(data.length >= UPDATE_HEADER_LEN + FEED_ENCODED_LEN, "AerePyth: update too short");
|
||||
require(bytes4(data[0:4]) == UPDATE_MAGIC, "AerePyth: bad magic");
|
||||
require(uint8(data[4]) == UPDATE_VERSION, "AerePyth: bad version");
|
||||
uint256 sigCount = uint8(data[5]);
|
||||
require(sigCount > 0, "AerePyth: no signatures");
|
||||
// Exact consumption. Nothing is inferred from data.length; the length
|
||||
// the header ANNOUNCES has to be the length that was sent.
|
||||
require(
|
||||
data.length == UPDATE_HEADER_LEN + FEED_ENCODED_LEN + sigCount * SIG_LEN,
|
||||
"AerePyth: bad payload length"
|
||||
);
|
||||
feed = abi.decode(data[UPDATE_HEADER_LEN:UPDATE_HEADER_LEN + FEED_ENCODED_LEN], (PythPriceFeed));
|
||||
sigs = data[UPDATE_HEADER_LEN + FEED_ENCODED_LEN:];
|
||||
}
|
||||
|
||||
function _verifyAndStore(PythPriceFeed memory feed, bytes calldata sigs) internal {
|
||||
uint256 sigCount = sigs.length / SIG_LEN;
|
||||
require(sigCount >= threshold, "AerePyth: below threshold");
|
||||
|
||||
bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(abi.encode(feed))));
|
||||
bytes32 digest = _updateDigest(feed);
|
||||
|
||||
address[] memory seen = new address[](sigCount);
|
||||
uint256 validCount = 0;
|
||||
|
||||
@ -38,9 +38,12 @@ import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
|
||||
* - native AERE: fixed 2.8B from genesis minus burned
|
||||
* balance at 0x...dEaD.
|
||||
*
|
||||
* The Foundation off-chain ingester (Python cron on Hostinger VPS,
|
||||
* Ledger-signed) collects these reserve numbers, builds the Merkle
|
||||
* tree, and posts the root via proposeSnapshot.
|
||||
* The Foundation off-chain ingester (Python cron on Hostinger VPS)
|
||||
* collects these reserve numbers, builds the Merkle tree, and posts
|
||||
* the root via proposeSnapshot, signed by the Foundation owner
|
||||
* account 0x0243A4f4 (measured on chain 2800: a single-key EOA
|
||||
* today, not a multisig). Hardware-wallet storage of that key is
|
||||
* NOT asserted here; the chain cannot attest to it.
|
||||
*
|
||||
* IMMUTABILITY:
|
||||
* - CHALLENGE_WINDOW constant.
|
||||
|
||||
@ -7,7 +7,17 @@ pragma solidity 0.8.23;
|
||||
* ML-KEM-768 is AERE's post-quantum CONFIDENTIALITY primitive: every other
|
||||
* native PQC precompile is a signature or hash (post-quantum authenticity),
|
||||
* this one makes a Module-Lattice KEM key-agreement transcript verifiable
|
||||
* on chain 2800.
|
||||
* on a chain where the 0x0AE6 precompile is activated.
|
||||
*
|
||||
* @dev HONEST SCOPE - TESTNET ONLY (as of 2026-07-20). The 0x0AE6 precompile this
|
||||
* contract wraps is live on the AERE TESTNET ONLY. It is NOT activated on
|
||||
* mainnet chain 2800, which has exactly the FIVE precompiles 0x0AE1..0x0AE5
|
||||
* (activated block 9,189,161). On mainnet today a staticcall to 0x0AE6 hits an
|
||||
* empty account and returns success with EMPTY data, so every length check
|
||||
* below fails and this contract reports failure rather than a false positive.
|
||||
* Deploying it on mainnet 2800 is therefore safe but useless until activation,
|
||||
* which is founder and external-audit gated. Nothing here changes AERE
|
||||
* consensus, which remains classical secp256k1 ECDSA QBFT.
|
||||
*
|
||||
* The precompile performs a DETERMINISTIC ML-KEM.Encaps: given an
|
||||
* encapsulation key `ek` (1184 bytes) and 32-byte encapsulation coins `m`,
|
||||
@ -32,7 +42,8 @@ pragma solidity 0.8.23;
|
||||
* to screen a malformed ek.
|
||||
*/
|
||||
contract AereMLKEM768 {
|
||||
/// @dev Native precompile address (AERE Besu PQC fork).
|
||||
/// @dev Native precompile address (AERE Besu PQC fork). TESTNET ONLY: not
|
||||
/// activated on mainnet chain 2800. See the HONEST SCOPE note above.
|
||||
address public constant MLKEM768 = 0x0000000000000000000000000000000000000AE6;
|
||||
|
||||
uint256 public constant EK_LEN = 1184; // encapsulation key
|
||||
|
||||
@ -14,7 +14,7 @@ pragma solidity 0.8.23;
|
||||
* does NOT change AERE consensus: blocks are still produced and signed by
|
||||
* Besu QBFT validators with ECDSA (secp256k1). What is new and live is the
|
||||
* set of native precompiles this contract calls, activated on mainnet 2800
|
||||
* at block 9,189,161. Each precompile wraps the audited Bouncy Castle NIST
|
||||
* at block 9,189,161. Each precompile wraps the Bouncy Castle NIST
|
||||
* verifiers. This contract makes those precompiles USABLE end to end: it
|
||||
* is the difference between "PQC verification is callable" and
|
||||
* "quantum-durable attestations are recorded on-chain".
|
||||
|
||||
@ -24,7 +24,7 @@ pragma solidity 0.8.23;
|
||||
* consensus: blocks are still produced and signed by Besu QBFT validators with
|
||||
* classical ECDSA (secp256k1). What is new and live is the set of native
|
||||
* precompiles this contract calls (mainnet 2800, activation block 9,189,161),
|
||||
* each wrapping the audited Bouncy Castle NIST verifiers. No funds are ever held.
|
||||
* each wrapping the Bouncy Castle NIST verifiers. No funds are ever held.
|
||||
*
|
||||
* @dev The precompile wire format is IDENTICAL to AerePQCAttestation (proven against
|
||||
* the live mainnet precompiles via eth_call). See that contract's NatSpec and
|
||||
|
||||
@ -40,7 +40,7 @@ interface IAerePQCMessageVerifier {
|
||||
* HONEST SCOPE. Application-layer PQC signature verification. This does NOT change
|
||||
* AERE consensus, which remains classical ECDSA QBFT (Foundation validators). What
|
||||
* is new and live is the set of native precompiles this contract calls, activated
|
||||
* on mainnet 2800 at block 9,189,161, each wrapping the audited Bouncy Castle NIST
|
||||
* on mainnet 2800 at block 9,189,161, each wrapping the Bouncy Castle NIST
|
||||
* verifiers. This contract makes those precompiles USABLE for the bridging/oracle
|
||||
* message-authentication pattern.
|
||||
*
|
||||
|
||||
@ -25,7 +25,7 @@ pragma solidity 0.8.23;
|
||||
* can set an epoch available is `attest`, which strictly requires >= M
|
||||
* distinct registered-member signatures. The `admin` role can only manage
|
||||
* committee membership / threshold (registrar duties, intended to be the
|
||||
* Foundation multisig in production) and can be permanently frozen; it can
|
||||
* Foundation account in production; a single-key EOA today, not yet a multisig) and can be permanently frozen; it can
|
||||
* never mark data available without member signatures.
|
||||
*/
|
||||
contract AereDACommittee {
|
||||
|
||||
@ -21,6 +21,25 @@ import "./AereRollupSettlementV2.sol";
|
||||
* the factory deploys (per-registration override allowed, 0 → default),
|
||||
* mirroring DEFAULT_CHALLENGE_WINDOW.
|
||||
*
|
||||
* WHAT EVERY ROLLUP STAMPED BY THIS FACTORY STILL DOES NOT GET.
|
||||
* Added 2026-08-01, alongside the same warning restored in the template.
|
||||
* The settlement template this factory deploys carries NO FRAUD-PROOF
|
||||
* VERIFIER. Nothing in it checks whether a proposed state root is
|
||||
* correct. Its challenge game is Phase 1: a challenger posts a bond and
|
||||
* resolveChallenge is callable only by the sequencer, which is the party
|
||||
* whose own root is being challenged; a correct challenge can be defeated
|
||||
* by the accused, and the honest challenger then loses the bond. The
|
||||
* permissionless resolveChallengeExpired path only covers a sequencer
|
||||
* that ignores the challenge past the grace window. A rollup registered
|
||||
* here is therefore secured by its sequencer's honesty plus off-chain
|
||||
* cost, NOT by a proof, and must not be described as trust-minimised on
|
||||
* the strength of the challenge window. Phase 2 replaces sequencer-side
|
||||
* resolution with a real on-chain fraud-proof verifier.
|
||||
*
|
||||
* That paragraph is comments only: Solidity comments emit no
|
||||
* instructions, so the executable runtime bytecode is unchanged and only
|
||||
* the embedded metadata hash moves.
|
||||
*
|
||||
* IMMUTABILITY at factory deploy: BOND_TOKEN, SINK, REGISTRATION_BOND,
|
||||
* MIN_SINK_BPS, DEFAULT_CHALLENGE_WINDOW, DEFAULT_DEFENSE_GRACE,
|
||||
* FOUNDATION. The factory cannot retroactively change a registered
|
||||
|
||||
@ -52,6 +52,38 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
* Everything else (immutability model, revenue split maths, governance-
|
||||
* lite sequencer/owner rotation) is behaviourally identical to V1.
|
||||
*
|
||||
* ───────────────── WHAT THIS CONTRACT STILL DOES NOT DO ─────────────────
|
||||
* RESTORED 2026-08-01. V1 carried this warning twice, in its header and
|
||||
* above resolveChallenge, and V2 shipped without it. Removing an honest
|
||||
* warning while fixing bugs is a regression in its own right, so it is
|
||||
* put back, and stated more plainly than V1 stated it:
|
||||
*
|
||||
* THERE IS NO FRAUD-PROOF VERIFIER HERE. Nothing in this contract
|
||||
* checks whether a proposed state root is correct. The challenge game
|
||||
* is Phase 1: a challenger posts a bond, and the outcome is decided by
|
||||
* `resolveChallenge`, which is callable ONLY by the sequencer — that
|
||||
* is, by the party whose own root is being challenged. A challenger
|
||||
* who is right can still be ruled against by the accused, and in that
|
||||
* case (accepted=false) the challenger's bond is SLASHED. The
|
||||
* `resolveChallengeExpired` path only lets a challenge that the
|
||||
* sequencer simply ignored past the grace period reject the root.
|
||||
*
|
||||
* So the security of a rollup settled through this contract rests on
|
||||
* the sequencer's honesty plus the reputational and off-chain cost of
|
||||
* misbehaving. It does NOT rest on a proof. Do not describe a rollup
|
||||
* deployed by AereRaaSFactoryV2 as trust-minimised on the strength of
|
||||
* this challenge window.
|
||||
*
|
||||
* Phase 2 replaces sequencer-side resolution with a real on-chain
|
||||
* fraud-proof verifier. Until that ships, the sentence above is the
|
||||
* accurate description of this contract.
|
||||
*
|
||||
* NOTE ON BYTECODE IDENTITY: this restoration adds comments only. Solidity
|
||||
* comments emit no instructions, so the executable runtime bytecode is
|
||||
* unchanged; the embedded metadata hash does move, so a fresh compile of
|
||||
* this file is expected to be an executable-identical, metadata-differing
|
||||
* ("partial") match against templates deployed before 2026-08-01.
|
||||
*
|
||||
* IMMUTABILITY at deploy: rollupId, sink, debtToken, challengeWindow,
|
||||
* DEFENSE_GRACE, sinkBps/rollupBps/sequencerBps (sum 10_000),
|
||||
* minChallengeBond.
|
||||
@ -258,6 +290,14 @@ contract AereRollupSettlementV2 is ReentrancyGuard {
|
||||
/// accepted=false → sequencer defends a valid root: challenge
|
||||
/// defeated, root survives, challenger's bond
|
||||
/// SLASHED to the AereSink (grief is now costly).
|
||||
///
|
||||
/// HONEST LIMIT, restored 2026-08-01 (V1 stated this, V2 had dropped it):
|
||||
/// this function verifies NOTHING about the state root. The judge of a
|
||||
/// challenge is `sequencer`, which is the party being challenged. A
|
||||
/// correct challenge can be defeated by the accused, and the honest
|
||||
/// challenger then loses the bond. Phase 2 replaces this with an
|
||||
/// on-chain fraud-proof verifier; until then the challenge window is a
|
||||
/// bonded dispute with a self-interested arbiter, not a proof.
|
||||
function resolveChallenge(uint256 epoch, bool accepted) external nonReentrant {
|
||||
if (msg.sender != sequencer) revert NotSequencer();
|
||||
StateRoot storage s = roots[epoch];
|
||||
|
||||
@ -25,7 +25,7 @@ import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
* so block explorers / the AERE indexer can display balances. The WRITE side
|
||||
* of ERC-20 is deliberately bricked.
|
||||
*
|
||||
* Owner = Foundation multisig. Ownable only manages the attestor allowlist;
|
||||
* Owner = Foundation account (single-key EOA today, not a multisig). Ownable only manages the attestor allowlist;
|
||||
* the owner cannot move, mint to itself for transfer, or reduce balances.
|
||||
*/
|
||||
contract AerePointsLedger is Ownable {
|
||||
|
||||
@ -29,7 +29,7 @@ import "./interfaces/ISeason1.sol";
|
||||
* signature, not msg.sender, authorizes the credit.
|
||||
*
|
||||
* This contract must be added as an authorized attestor on the ledger by the
|
||||
* Foundation post-deploy. Owner = Foundation multisig.
|
||||
* Foundation post-deploy. Owner = Foundation account (single-key EOA today, not a multisig).
|
||||
*/
|
||||
contract AereQuestAttestor is Ownable, ReentrancyGuard, EIP712, IAereQuestAttestor {
|
||||
using ECDSA for bytes32;
|
||||
|
||||
@ -14,7 +14,7 @@ import "./interfaces/ISeason1.sol";
|
||||
* A quest is keyed by a bytes32 questId (e.g. keccak256("create-passkey")).
|
||||
*
|
||||
* This contract holds NO funds and issues NO credits. It is pure config that
|
||||
* the attestor and referral registry consult. Owner = Foundation multisig.
|
||||
* the attestor and referral registry consult. Owner = Foundation account (single-key EOA today, not a multisig).
|
||||
*/
|
||||
contract AereQuestRegistry is Ownable, IAereQuestRegistry {
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ import "./interfaces/ISeason1.sol";
|
||||
*
|
||||
* This contract is authorized as an attestor on the ledger by the Foundation
|
||||
* and credits referrers directly via LEDGER.credit(...). It holds no funds.
|
||||
* Owner = Foundation multisig.
|
||||
* Owner = Foundation account (single-key EOA today, not a multisig).
|
||||
*
|
||||
* DISCLAIMER: AP is not a token, has no guaranteed value, and confers no
|
||||
* right to returns. Rewards depend on the network growing and are not
|
||||
|
||||
@ -20,7 +20,7 @@ import "./interfaces/ISeason1.sol";
|
||||
* │ funding transaction, and ONLY AFTER an external audit. │
|
||||
* │ - MUST BE EXTERNALLY AUDITED before it is funded. It will custody real│
|
||||
* │ AERE once funded; treat every line as adversarial surface. │
|
||||
* │ - The owner (Foundation multisig) can pause, exclude sybils before │
|
||||
* │ - The owner (Foundation account, single-key EOA today, not a multisig) can pause, exclude sybils before │
|
||||
* │ claims begin, and claw back ONLY the UNCLAIMED remainder AFTER a │
|
||||
* │ long expiry. The owner CANNOT redirect a valid, vested claim away │
|
||||
* │ from its rightful account, and cannot shorten the claim window. │
|
||||
|
||||
@ -50,7 +50,7 @@ import "./interfaces/ISeason1.sol";
|
||||
* transaction, and ONLY AFTER an external audit.
|
||||
* - MUST BE EXTERNALLY AUDITED before it is funded. It will custody real
|
||||
* AERE once funded; treat every line as adversarial surface.
|
||||
* - The owner (Foundation multisig) can pause, exclude sybils that have NOT
|
||||
* - The owner (Foundation account, single-key EOA today, not a multisig) can pause, exclude sybils that have NOT
|
||||
* yet claimed, and claw back only balance beyond the declared reserve ONLY
|
||||
* AFTER a long expiry and ONLY while not paused. The owner CANNOT exclude an
|
||||
* account that has already claimed, CANNOT redirect or freeze a claim that is
|
||||
|
||||
@ -187,11 +187,15 @@ contract AereSettlementHub is Ownable, ReentrancyGuard {
|
||||
emit SolverBondDeposited(asset, solver, amount);
|
||||
}
|
||||
|
||||
/// @notice Owner slashes a solver bond. The destination is the immutable SINK
|
||||
/// and the owner cannot redirect it, but delivery is BEST EFFORT: the
|
||||
/// flush below is a low-level call whose failure is swallowed, so a
|
||||
/// successful slash does NOT prove the amount reached the sink.
|
||||
function slashSolverBond(address asset, address solver, uint256 amount) external onlyOwner nonReentrant {
|
||||
uint256 cur = solverBond[asset][solver];
|
||||
if (cur < amount) revert InsufficientBond(cur, amount);
|
||||
solverBond[asset][solver] = cur - amount;
|
||||
// Slash → AereSink to feed the burn flywheel.
|
||||
// Best-effort push to the immutable SINK; see the failure branch below.
|
||||
IERC20(asset).approve(SINK, amount);
|
||||
(bool ok, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", asset, amount));
|
||||
if (!ok) {
|
||||
|
||||
@ -251,6 +251,10 @@ contract AereSettlementHubV2 is Ownable, ReentrancyGuard {
|
||||
emit SolverBondWithdrawn(asset, msg.sender, amount);
|
||||
}
|
||||
|
||||
/// @notice Owner slashes a solver bond. The destination is the immutable SINK
|
||||
/// and the owner cannot redirect it, but delivery is BEST EFFORT: the
|
||||
/// flush below is a low-level call whose failure is swallowed, so a
|
||||
/// successful slash does NOT prove the amount reached the sink.
|
||||
function slashSolverBond(address asset, address solver, uint256 amount) external onlyOwner nonReentrant {
|
||||
uint256 cur = solverBond[asset][solver];
|
||||
if (cur < amount) revert InsufficientBond(cur, amount);
|
||||
@ -258,7 +262,7 @@ contract AereSettlementHubV2 is Ownable, ReentrancyGuard {
|
||||
// The slashed bond is no longer owed to the solver, so it stops being a
|
||||
// committed liability. It is destined for the sink.
|
||||
committedLiabilities[asset] -= amount;
|
||||
// Slash then AereSink to feed the burn flywheel.
|
||||
// Best-effort push to the immutable SINK; see the failure branch below.
|
||||
IERC20(asset).approve(SINK, amount);
|
||||
(bool ok, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", asset, amount));
|
||||
if (!ok) {
|
||||
|
||||
@ -35,7 +35,8 @@ interface IUSDCeMintable {
|
||||
* releases the locked USDC.
|
||||
*
|
||||
* Trusted-router enrollment:
|
||||
* Owner (Foundation 2-of-3) registers exactly ONE router per remote
|
||||
* Owner (the Foundation account, a single-key EOA today, not a
|
||||
* multisig) registers exactly ONE router per remote
|
||||
* domain. The address is bytes32-padded as required by Hyperlane.
|
||||
* Handle() rejects any sender not in the enrolment.
|
||||
*
|
||||
|
||||
@ -13,7 +13,7 @@ pragma solidity 0.8.23;
|
||||
* For each validator a node operator registers:
|
||||
* - validatorAddress QBFT signer (the address Besu uses to sign blocks)
|
||||
* - operatorLabel human-readable name (e.g. "P2P Validator", "Figment")
|
||||
* - operatorRegion e.g. "EU-Central / DE / example-dc-1"
|
||||
* - operatorRegion e.g. "EU-Central / DE"
|
||||
* - binaryDigest SHA256 of the Besu image they're running
|
||||
* - manifestUri where the binary digest's signed manifest lives
|
||||
* (typically the published .well-known/besu-image-digest)
|
||||
|
||||
@ -18,7 +18,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
||||
/// register their own program with a human-readable name + URL. The Foundation
|
||||
/// can disable malicious programs but cannot disable legitimate ones.
|
||||
/// - Proof submission emits ProofVerified with the program key indexed, so
|
||||
/// off-chain consumers (Bank28 KYC, dApp loyalty, exchange withdrawal proofs)
|
||||
/// off-chain consumers (partner KYC, dApp loyalty, exchange withdrawal proofs)
|
||||
/// can subscribe to events filtered by their program of interest.
|
||||
///
|
||||
/// The verifyXOnly view functions provide free verification without recording,
|
||||
|
||||
@ -2,6 +2,13 @@
|
||||
"name": "aere-contracts",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "hardhat clean",
|
||||
"compile": "hardhat compile",
|
||||
"test": "hardhat test",
|
||||
"test:pqc": "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",
|
||||
"coverage": "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}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@noble/post-quantum": "^0.6.1",
|
||||
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Formally analysed properties — AERE PQC contracts
|
||||
# Formally analysed properties, AERE PQC contracts
|
||||
|
||||
Symbolic execution with **halmos 0.3.3** (Z3 4.12.6) over **Solidity 0.8.23** builds
|
||||
of the **LIVE** contracts:
|
||||
@ -86,20 +86,20 @@ vacuously passing.
|
||||
|---|----------|------|--------|
|
||||
| R1 | **Fail-closed**: a **REVOKED** or **UNKNOWN** algorithm id can **never** pass `verify()`, for any pubkey/message/signature and regardless of what the routed verifier would return. | `check_revokedOrUnknownNeverVerifies` | **PASS** |
|
||||
| R2 | Status transitions are **owner-only**: a non-owner `setStatus` (and `setSuccessor`) call cannot change registry state. | `check_setStatusIsOwnerOnly`, `check_setSuccessorIsOwnerOnly` | **PASS** |
|
||||
| R3 | `resolveActive` **terminates** on any symbolic start id (returns an ACTIVE row or reverts), never diverging — including on a deliberately-seeded successor **cycle**. | `check_resolveActiveTerminatesAndReturnsActive` | **PASS** |
|
||||
| R3 | `resolveActive` **terminates** on any symbolic start id (returns an ACTIVE row or reverts), never diverging, including on a deliberately-seeded successor **cycle**. | `check_resolveActiveTerminatesAndReturnsActive` | **PASS** |
|
||||
|
||||
---
|
||||
|
||||
## Assumptions (these bound the strength of every "PASS" above)
|
||||
|
||||
1. **Precompile as an uninterpreted oracle (central).** halmos cannot execute AERE's
|
||||
native PQC precompiles (`0x0AE1..0x0AE4`) — they are Bouncy-Castle native code on
|
||||
native PQC precompiles (`0x0AE1..0x0AE4`), they are Bouncy-Castle native code on
|
||||
the Besu fork, not EVM bytecode. The tests **etch a 1-slot EVM stub** at the scheme's
|
||||
precompile address and drive its 32-byte return word with a **symbolic** value, so
|
||||
halmos explores both the "precompile says valid" and "precompile says invalid"
|
||||
branches on every call. This is *strictly more adversarial* than a faithful
|
||||
precompile: the safety properties are shown to hold no matter what the precompile
|
||||
returns. It proves **nothing** about Falcon/ML-DSA/SLH-DSA cryptographic soundness —
|
||||
returns. It proves **nothing** about Falcon/ML-DSA/SLH-DSA cryptographic soundness,
|
||||
that is covered elsewhere (NIST KAT vectors + live `eth_call` checks), not here.
|
||||
|
||||
2. **keccak injectivity.** halmos models `keccak256` as an injective, collision-free
|
||||
@ -108,7 +108,7 @@ vacuously passing.
|
||||
|
||||
3. **Bounded symbolic `bytes` lengths.** Dynamic `bytes` parameters are explored only
|
||||
at the lengths passed via `--default-bytes-lengths`:
|
||||
- Suite 1 used `0,32,65,897,1312,1793` — this **covers the exact valid pubkey
|
||||
- Suite 1 used `0,32,65,897,1312,1793`, this **covers the exact valid pubkey
|
||||
lengths of all four schemes** (897 Falcon-512, 1793 Falcon-1024, 1312 ML-DSA-44,
|
||||
32 SLH-DSA-128s) plus invalid lengths, so P4's accept/reject boundary is genuinely
|
||||
exercised, not vacuous. Falcon signatures reach the verifier at length 65.
|
||||
@ -136,7 +136,7 @@ vacuously passing.
|
||||
|
||||
## What is NOT claimed
|
||||
|
||||
- Not "formally verified" as a blanket statement — only P1–P4, R1–R3 within the bounds above.
|
||||
- Not "formally verified" as a blanket statement, only P1–P4, R1–R3 within the bounds above.
|
||||
- No claim about the PQC precompiles' cryptographic correctness (oracle assumption).
|
||||
- No claim about consensus: AERE consensus remains classical ECDSA QBFT; these are
|
||||
application-layer contract properties only.
|
||||
@ -144,15 +144,15 @@ vacuously passing.
|
||||
|
||||
---
|
||||
|
||||
# z3 SMT DESIGN MODELS (formal-consensus/*.py) — inductive accounting proofs
|
||||
# z3 SMT DESIGN MODELS (formal-consensus/*.py), inductive accounting proofs
|
||||
|
||||
A second, complementary formal layer sits in `formal-consensus/`. These are **z3
|
||||
SMT design models** in the style of `formal-consensus/spokepool_smt.py` (the model
|
||||
SMT design models** in the style of `aere-research/formal-consensus/spokepool_smt.py` (the model
|
||||
that found the real SpokePool bond-accounting bug). Each models a contract's
|
||||
money/authorization logic as an **inductive transition system** over unbounded
|
||||
integers (the same abstraction solc's SMTChecker uses by default), proving the key
|
||||
safety invariant is **inductive** — base case holds, and every operation preserves
|
||||
it — and includes **negative controls** that reproduce the corresponding real bug
|
||||
safety invariant is **inductive**, base case holds, and every operation preserves
|
||||
it, and includes **negative controls** that reproduce the corresponding real bug
|
||||
when a guard is removed (so the proofs are demonstrably non-vacuous).
|
||||
|
||||
**Scope:** these check the **DESIGN math** (the accounting/authorization
|
||||
@ -166,7 +166,7 @@ halmos bytecode-level suites above; they do not replace them.
|
||||
python formal-consensus/settlementhub_smt.py # exit 0 on all-proved
|
||||
python formal-consensus/lending_liquidation_smt.py
|
||||
python formal-consensus/agentdid_session_smt.py
|
||||
python formal-consensus/spokepool_smt.py # the original reference model
|
||||
python aere-research/formal-consensus/spokepool_smt.py # the original reference model
|
||||
# --- the four LIVE (deployed 2026-07-12) PQC contracts ---
|
||||
python formal-consensus/pqckeyregistry_smt.py # AerePQCKeyRegistry 0x1eCa..3691
|
||||
python formal-consensus/hybridauthorizer_smt.py # AereHybridAuthorizer 0x168F..82E0
|
||||
@ -182,7 +182,7 @@ violation is `sat`), so every PROVED is demonstrably non-vacuous. `forge`/`halmo
|
||||
are not installed in this environment; these z3 DESIGN models are the runnable proof
|
||||
here and complement (do not replace) the halmos BYTECODE suites above.
|
||||
|
||||
### AerePQCKeyRegistry (`0x1eCa3c5ADcBD0b22636D8672b00faC6D89363691`) — `pqckeyregistry_smt.py`
|
||||
### AerePQCKeyRegistry (`0x1eCa3c5ADcBD0b22636D8672b00faC6D89363691`), `pqckeyregistry_smt.py`
|
||||
|
||||
| # | Property | Result |
|
||||
|---|----------|--------|
|
||||
@ -192,9 +192,9 @@ here and complement (do not replace) the halmos BYTECODE suites above.
|
||||
| K4 | **Rotation preserves the owner binding** (`successor.owner == predecessor.owner`); old leaves ACTIVE | **PROVED** |
|
||||
| NC1–4 | owner-less challenge (cross-identity replay), nonce-less challenge (same-identity replay), missing-REVOKED gate, arbitrary-owner rotate | **CEX** (all fire) |
|
||||
|
||||
Assumptions: **A1 signature-soundness** (precompile accepts only a genuine signature over the exact message — out of scope, NIST-KAT/eth_call elsewhere); **A2 keccak injectivity**; design model, not bytecode.
|
||||
Assumptions: **A1 signature-soundness** (precompile accepts only a genuine signature over the exact message, out of scope, NIST-KAT/eth_call elsewhere); **A2 keccak injectivity**; design model, not bytecode.
|
||||
|
||||
### AereHybridAuthorizer (`0x168F2A6a3071e7654CF1784a6f5d7BC8e1a582E0`) — `hybridauthorizer_smt.py`
|
||||
### AereHybridAuthorizer (`0x168F2A6a3071e7654CF1784a6f5d7BC8e1a582E0`), `hybridauthorizer_smt.py`
|
||||
|
||||
| # | Property | Result |
|
||||
|---|----------|--------|
|
||||
@ -207,7 +207,7 @@ Assumptions: **A1 signature-soundness** (precompile accepts only a genuine signa
|
||||
|
||||
Assumptions: **A1** registry `verify` is itself fail-closed (halmos R1 / `cryptoregistry_smt.py`); **A2** loop bound is the source's structural bound; design model.
|
||||
|
||||
### AereAgentDID (`0xce641d7d7C10553D82b06B7C21d423550e7522C5`) — `agentdid_lifecycle_smt.py`
|
||||
### AereAgentDID (`0xce641d7d7C10553D82b06B7C21d423550e7522C5`), `agentdid_lifecycle_smt.py`
|
||||
|
||||
Complements `agentdid_session_smt.py` (spend cap C1, gating C2, anti-replay C3, expiry C4).
|
||||
|
||||
@ -220,9 +220,9 @@ Complements `agentdid_session_smt.py` (spend cap C1, gating C2, anti-replay C3,
|
||||
| D3 | Containment envelope: a successful action stays within cap AND within expiry | **PROVED** |
|
||||
| NC1–3 | cached root-active flag, missing controller gate, missing signature gate | **CEX** (all fire) |
|
||||
|
||||
Assumptions: **A1 ecrecover-soundness**; **A2** the liveness check reads the LIVE registry status (no stale cache — this is the fact D1 exploits); design model.
|
||||
Assumptions: **A1 ecrecover-soundness**; **A2** the liveness check reads the LIVE registry status (no stale cache, this is the fact D1 exploits); design model.
|
||||
|
||||
### AereCryptoRegistry (`0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5`) — `cryptoregistry_smt.py`
|
||||
### AereCryptoRegistry (`0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5`), `cryptoregistry_smt.py`
|
||||
|
||||
Design-level **re-confirmation** of the halmos R1/R3 suite (the bytecode proof in
|
||||
`AereCryptoRegistry.symbolic.t.sol` remains authoritative).
|
||||
@ -240,13 +240,13 @@ the modelled guards**; every negative control fires (guards are load-bearing). *
|
||||
real counterexample to a should-hold invariant was found.** One benign, non-exploitable
|
||||
observation: `AereAgentDID.authorize`'s overflow guard `s.spent + amount < s.spent` is
|
||||
**dead code** under Solidity-0.8 checked arithmetic (the addition reverts before the
|
||||
comparison can be true) — it fails **safe** (revert = no state change), so it is a
|
||||
comparison can be true), it fails **safe** (revert = no state change), so it is a
|
||||
redundancy, **not a defect**. Consensus is unaffected: AERE remains classical ECDSA
|
||||
QBFT; these are application-layer contract properties only. "Proven" means only the
|
||||
specific properties + bounds + assumptions listed — **not** "formally verified" as a
|
||||
specific properties + bounds + assumptions listed, **not** "formally verified" as a
|
||||
blanket claim.
|
||||
|
||||
## AereSettlementHubV2 — `settlementhub_smt.py`
|
||||
## AereSettlementHubV2, `settlementhub_smt.py`
|
||||
|
||||
Contract: `contracts/settlement/AereSettlementHubV2.sol`. Invariant
|
||||
**`INV := balanceOf(hub) ≥ committedLiabilities[asset]`** (solvency / no-theft: a
|
||||
@ -265,10 +265,10 @@ permissionless `sweepResidual` can only ever move un-owed surplus, and
|
||||
| NC1 | V1 unbounded sweep (bound = balance, ignores committed) **steals owed funds** | **CEX** (sat) |
|
||||
| NC2 | buggy deposit committing the **full** amount (not amountAfterFee) **over-commits** | **CEX** (sat) |
|
||||
|
||||
## AereLendingMarket.liquidate — `lending_liquidation_smt.py`
|
||||
## AereLendingMarket.liquidate, `lending_liquidation_smt.py`
|
||||
|
||||
Contract: `contracts/lending/AereLendingMarket.sol` (the `liquidate` seize/clamp
|
||||
math, modelled at 18/18 decimals — see assumption A1 in the file).
|
||||
math, modelled at 18/18 decimals, see assumption A1 in the file).
|
||||
|
||||
| # | Property | Result |
|
||||
|---|----------|--------|
|
||||
@ -276,12 +276,12 @@ math, modelled at 18/18 decimals — see assumption A1 in the file).
|
||||
| L2 | insolvent-clamp branch: `value(pay) ≤ value(collateral seized)` (liquidator never overpays) | **PROVED** |
|
||||
| L3 | non-clamp branch: `value(received) ≤ valueToSeize` (no over-seize of VALUE) | **PROVED** |
|
||||
| L4 | `repaidScaled` (clamped to `debtOf`) `≤ debtOf` (debt bookkeeping no-underflow) | **PROVED** |
|
||||
| — | `scaleDown` floor-division monotonicity (supports A1) | **PROVED** |
|
||||
| L3-note | reverse direction `value(received) ≥ value(paid)` fails by ≤ wei-dust that rounds **toward the protocol** (the SAFE direction) — expected DeFi rounding, **NOT a bug** | demonstrated (sat, dust = 1 wei) |
|
||||
|, | `scaleDown` floor-division monotonicity (supports A1) | **PROVED** |
|
||||
| L3-note | reverse direction `value(received) ≥ value(paid)` fails by ≤ wei-dust that rounds **toward the protocol** (the SAFE direction), expected DeFi rounding, **NOT a bug** | demonstrated (sat, dust = 1 wei) |
|
||||
| NC1 | **without** the clamp, `seize` can exceed collateral → theft of pooled collateral | **CEX** (sat) |
|
||||
| NC2 | buggy clamp that **inflates** the reduced pay (`×(10000+bonus)/10000`) makes the liquidator **overpay** | **CEX** (sat) |
|
||||
|
||||
## AereAgentDID — `agentdid_session_smt.py`
|
||||
## AereAgentDID, `agentdid_session_smt.py`
|
||||
|
||||
Contract: `contracts/pqc/AereAgentDID.sol` (session `authorize` / gating /
|
||||
anti-replay). Invariant **`spent ≤ spendCap`** plus the containment gates.
|
||||
@ -304,7 +304,7 @@ anti-replay). Invariant **`spent ≤ spendCap`** plus the containment gates.
|
||||
compiled bytecode (that is the halmos layer's job).
|
||||
- **Standard ERC20** for the settlement hub (`transferFrom`/`transfer` move exactly
|
||||
`amount`). **Fee-on-transfer / rebasing tokens are OUT OF SCOPE** and would break
|
||||
the solvency invariant of any escrow — excluded by the asset-listing policy.
|
||||
the solvency invariant of any escrow, excluded by the asset-listing policy.
|
||||
- **Trusted `AereSink`** (pulls at most the approved amount; `flush` either succeeds
|
||||
or reverts).
|
||||
- **Lending decimals normalised to 18/18** for tractability; the `_scaleUp`/
|
||||
@ -322,7 +322,7 @@ No real counterexample was found in any of the three contracts: every stated
|
||||
safety invariant is **inductive under the modelled guards**, and all negative
|
||||
controls fire as expected (confirming the guards are load-bearing). The only `sat`
|
||||
result on a "should-hold" phrasing was the AereLendingMarket **L3-note**, which is
|
||||
a **1-wei rounding-dust** discrepancy in the protocol-favouring (safe) direction —
|
||||
a **1-wei rounding-dust** discrepancy in the protocol-favouring (safe) direction,
|
||||
documented above as expected DeFi rounding, **not a defect**. Consensus is
|
||||
unaffected: AERE remains classical ECDSA QBFT; these are application-layer
|
||||
contract properties only.
|
||||
|
||||
236
test/helpers/shutter-crypto.js
Normal file
236
test/helpers/shutter-crypto.js
Normal file
@ -0,0 +1,236 @@
|
||||
// shutter-crypto.js
|
||||
//
|
||||
// REAL threshold-BLS (BLS12-381) helpers for AereShutterMempoolV2, the
|
||||
// application-level Shutter-style anti-MEV encrypted mempool PoC on AERE
|
||||
// chain 2800. Uses @noble/curves (already in the repo node_modules).
|
||||
//
|
||||
// Scheme (Boldyreva threshold BLS + hashed-ElGamal encryption):
|
||||
// - Committee master secret s, Shamir-shared (t,N) over the scalar field Fr.
|
||||
// - Group public key PK = s * g2 (G2, the "encryption pubkey").
|
||||
// - Keyper pubkeys P_i = s_i * g2 (G2, published VSS commitments).
|
||||
// - Epoch identity H1 = hashToCurve(epoch tag) in G1.
|
||||
// - Epoch decrypt key DK = s * H1 (G1, threshold signature).
|
||||
// - Keyper share sig_i = s_i * H1 (G1).
|
||||
// - DK reconstruction DK = sum_{i in S} lambda_i * sig_i (Lagrange at 0).
|
||||
//
|
||||
// On-chain the pairing precompile (EIP-2537, addr 0x0f, VERIFIED live on 2800)
|
||||
// checks:
|
||||
// share valid : e(sig_i, g2) == e(H1, P_i) <=> e(sig_i,g2)*e(-H1,P_i)==1
|
||||
// DK valid : e(DK, g2) == e(H1, PK) <=> e(DK, g2)*e(-H1,PK )==1
|
||||
//
|
||||
// Encryption (hashed-ElGamal to the epoch), decryptable only with DK:
|
||||
// gt = e(H1, PK) in GT ; pick random r ; U = r*g2 ;
|
||||
// mask = keccak(gt^r) ; C = plaintext XOR mask ; ciphertext = (U, C).
|
||||
// Decrypt: gt^r = e(DK, U) ; mask = keccak(e(DK,U)) ; plaintext = C XOR mask.
|
||||
// (The GT->bytes masking is off-chain; on-chain we bind the plaintext to a
|
||||
// keccak commitment. DK correctness itself is pairing-verified on-chain.)
|
||||
|
||||
const { bls12_381 } = require("@noble/curves/bls12-381");
|
||||
const { keccak_256 } = require("@noble/hashes/sha3");
|
||||
const { randomBytes } = require("crypto");
|
||||
|
||||
const G1 = bls12_381.G1.ProjectivePoint;
|
||||
const G2 = bls12_381.G2.ProjectivePoint;
|
||||
const Fr = bls12_381.fields.Fr;
|
||||
const Fp2 = bls12_381.fields.Fp2;
|
||||
const Fp12 = bls12_381.fields.Fp12;
|
||||
const R = Fr.ORDER;
|
||||
const DST = "AERE-SHUTTER-V2-BLS12381G1-XMD:KECCAK-256_SSWU_RO_"; // documented epoch-identity DST
|
||||
|
||||
/* ----------------------------- serialization ----------------------------- */
|
||||
function fpTo64Hex(x) {
|
||||
// 48-byte big-endian field element, left-padded to 64 bytes (EIP-2537).
|
||||
const h = x.toString(16).padStart(96, "0");
|
||||
return "00".repeat(16) + h;
|
||||
}
|
||||
function g1Hex(P) {
|
||||
const a = P.toAffine();
|
||||
return "0x" + fpTo64Hex(a.x) + fpTo64Hex(a.y); // 128 bytes
|
||||
}
|
||||
function g2Hex(P) {
|
||||
const a = P.toAffine();
|
||||
// EIP-2537 Fp2 ordering: (c0, c1) for x then y.
|
||||
return (
|
||||
"0x" +
|
||||
fpTo64Hex(a.x.c0) + fpTo64Hex(a.x.c1) +
|
||||
fpTo64Hex(a.y.c0) + fpTo64Hex(a.y.c1)
|
||||
); // 256 bytes
|
||||
}
|
||||
// EIP-2537 -> noble point parsers (64-byte fields, last 48 bytes are the value)
|
||||
function chunkVal(chunk) { return BigInt("0x" + chunk.slice(32)); }
|
||||
function eip2537ToG1(h) {
|
||||
h = h.startsWith("0x") ? h.slice(2) : h;
|
||||
return G1.fromAffine({ x: chunkVal(h.slice(0, 128)), y: chunkVal(h.slice(128, 256)) });
|
||||
}
|
||||
function eip2537ToG2(h) {
|
||||
h = h.startsWith("0x") ? h.slice(2) : h;
|
||||
const xc0 = chunkVal(h.slice(0, 128)), xc1 = chunkVal(h.slice(128, 256));
|
||||
const yc0 = chunkVal(h.slice(256, 384)), yc1 = chunkVal(h.slice(384, 512));
|
||||
return G2.fromAffine({ x: Fp2.fromBigTuple([xc0, xc1]), y: Fp2.fromBigTuple([yc0, yc1]) });
|
||||
}
|
||||
function randScalar() {
|
||||
// uniform-ish nonzero scalar in [1, R-1]
|
||||
let x = 0n;
|
||||
while (x === 0n) x = BigInt("0x" + randomBytes(48).toString("hex")) % R;
|
||||
return x;
|
||||
}
|
||||
|
||||
/* --------------------------- polynomial / shamir -------------------------- */
|
||||
function evalPoly(coeffs, x) {
|
||||
// Horner in Fr
|
||||
let acc = 0n;
|
||||
for (let i = coeffs.length - 1; i >= 0; i--) acc = Fr.add(Fr.mul(acc, x), coeffs[i]);
|
||||
return acc;
|
||||
}
|
||||
// Lagrange coefficient lambda_i for interpolation AT 0 over index set S (1-based x = index).
|
||||
function lagrangeAtZero(indices, i) {
|
||||
let num = 1n, den = 1n;
|
||||
const xi = BigInt(i);
|
||||
for (const j of indices) {
|
||||
if (j === i) continue;
|
||||
const xj = BigInt(j);
|
||||
num = Fr.mul(num, Fr.sub(0n, xj)); // (0 - xj)
|
||||
den = Fr.mul(den, Fr.sub(xi, xj)); // (xi - xj)
|
||||
}
|
||||
return Fr.mul(num, Fr.inv(den));
|
||||
}
|
||||
|
||||
/* ------------------------------ committee setup --------------------------- */
|
||||
// Trusted-dealer Shamir setup for the PoC. Returns committee material.
|
||||
function setupCommittee(t, N) {
|
||||
const coeffs = [randScalar()]; // a0 = master secret s
|
||||
for (let k = 1; k < t; k++) coeffs.push(randScalar());
|
||||
const s = coeffs[0];
|
||||
const shares = []; // {index, secret}
|
||||
const pubkeys = []; // P_i hex (G2), index-aligned to keypers[i] (i=0..N-1 -> x=i+1)
|
||||
for (let i = 1; i <= N; i++) {
|
||||
const si = evalPoly(coeffs, BigInt(i));
|
||||
shares.push({ index: i, secret: si });
|
||||
pubkeys.push(g2Hex(G2.BASE.multiply(si)));
|
||||
}
|
||||
const PKpoint = G2.BASE.multiply(s);
|
||||
const PK = g2Hex(PKpoint); // group pubkey (encryption key)
|
||||
return { t, N, s, shares, pubkeys, PK, PKpoint, g2Gen: g2Hex(G2.BASE) };
|
||||
}
|
||||
|
||||
/* ------------------------------- epoch identity --------------------------- */
|
||||
function epochIdentity(epochId) {
|
||||
const tag = Buffer.from(`AERE-SHUTTER-EPOCH:${epochId}`, "utf8");
|
||||
const H1 = bls12_381.G1.hashToCurve(tag, { DST }); // RFC9380 hash-to-curve, cofactor-cleared
|
||||
const P = G1.fromHex(H1.toHex(true)); // normalize to ProjectivePoint
|
||||
return { H1: g1Hex(P), H1neg: g1Hex(P.negate()), point: P };
|
||||
}
|
||||
|
||||
/* ------------------------------- shares / DK ------------------------------ */
|
||||
function makeShare(secret_si, epochPoint) {
|
||||
return g1Hex(epochPoint.multiply(secret_si)); // sig_i = s_i * H1 (G1)
|
||||
}
|
||||
function reconstructDK(epochPoint, committee, subsetIndices) {
|
||||
// subsetIndices are 1-based keyper x-coords (length >= t)
|
||||
let acc = G1.ZERO;
|
||||
for (const i of subsetIndices) {
|
||||
const share = committee.shares.find((s) => s.index === i);
|
||||
const lam = lagrangeAtZero(subsetIndices, i);
|
||||
const sig_i = epochPoint.multiply(share.secret); // = s_i*H1
|
||||
acc = acc.add(sig_i.multiply(lam));
|
||||
}
|
||||
return { DK: g1Hex(acc), point: acc }; // should equal s*H1
|
||||
}
|
||||
|
||||
/* ------------------------------- encryption ------------------------------- */
|
||||
function fp12Bytes(z) {
|
||||
// deterministic flatten of an Fp12 element to bytes (12 * 48-byte coords).
|
||||
const flat = [
|
||||
z.c0.c0.c0, z.c0.c0.c1, z.c0.c1.c0, z.c0.c1.c1, z.c0.c2.c0, z.c0.c2.c1,
|
||||
z.c1.c0.c0, z.c1.c0.c1, z.c1.c1.c0, z.c1.c1.c1, z.c1.c2.c0, z.c1.c2.c1,
|
||||
];
|
||||
return Buffer.concat(flat.map((x) => Buffer.from(x.toString(16).padStart(96, "0"), "hex")));
|
||||
}
|
||||
function maskFromGT(gt, len) {
|
||||
// expand keccak(gt-bytes || counter) to len bytes
|
||||
const base = fp12Bytes(gt);
|
||||
const out = Buffer.alloc(len);
|
||||
let off = 0, ctr = 0;
|
||||
while (off < len) {
|
||||
const blk = Buffer.from(keccak_256(Buffer.concat([base, Buffer.from([ctr & 0xff])])));
|
||||
const n = Math.min(32, len - off);
|
||||
blk.copy(out, off, 0, n);
|
||||
off += n; ctr++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Encrypt plaintext (Buffer) to the epoch. Returns ciphertext bytes = U(256) || C(len).
|
||||
function encrypt(committee, epochId, plaintext) {
|
||||
const { point: H1 } = epochIdentity(epochId);
|
||||
const PKpt = committee.PKpoint || eip2537ToG2(committee.PK);
|
||||
const r = randScalar();
|
||||
const U = G2.BASE.multiply(r); // ephemeral
|
||||
const gt = bls12_381.pairing(H1, PKpt); // e(H1, PK)
|
||||
const gtr = Fp12.pow(gt, r); // gt^r
|
||||
const mask = maskFromGT(gtr, plaintext.length);
|
||||
const C = Buffer.from(plaintext.map((b, i) => b ^ mask[i]));
|
||||
const Ubuf = Buffer.from(g2Hex(U).slice(2), "hex");
|
||||
return { ciphertext: "0x" + Buffer.concat([Ubuf, C]).toString("hex"), U: g2Hex(U) };
|
||||
}
|
||||
// Decrypt with reconstructed DK point. ciphertext hex -> plaintext Buffer.
|
||||
function decrypt(dkPoint, ciphertextHex) {
|
||||
const buf = Buffer.from(ciphertextHex.slice(2), "hex");
|
||||
const Ubuf = buf.subarray(0, 256);
|
||||
const C = buf.subarray(256);
|
||||
const U = eip2537ToG2(Ubuf.toString("hex"));
|
||||
const gtr = bls12_381.pairing(dkPoint, U); // e(DK, U) == gt^r
|
||||
const mask = maskFromGT(gtr, C.length);
|
||||
return Buffer.from(C.map((b, i) => b ^ mask[i]));
|
||||
}
|
||||
|
||||
/* ----------------------------- commit / merkle ---------------------------- */
|
||||
const { AbiCoder, keccak256, solidityPacked } = require("ethers");
|
||||
const abi = AbiCoder.defaultAbiCoder();
|
||||
|
||||
function commitmentOf(plaintextHex, openingHex) {
|
||||
return keccak256(abi.encode(["bytes", "bytes32"], [plaintextHex, openingHex]));
|
||||
}
|
||||
// Merkle over ordered positions. leaf = keccak(bytes.concat(keccak(abi.encode(epochId,pos,commitment)))).
|
||||
function leafOf(epochId, pos, commitment) {
|
||||
const inner = keccak256(abi.encode(["uint64", "uint256", "bytes32"], [epochId, pos, commitment]));
|
||||
return keccak256(solidityPacked(["bytes32"], [inner]));
|
||||
}
|
||||
function hashPair(a, b) {
|
||||
const [x, y] = a.toLowerCase() <= b.toLowerCase() ? [a, b] : [b, a]; // OZ sorted pairs
|
||||
return keccak256(solidityPacked(["bytes32", "bytes32"], [x, y]));
|
||||
}
|
||||
// Build an OZ-compatible sorted-pair Merkle tree; returns {root, proofs[]}.
|
||||
function buildMerkle(leaves) {
|
||||
if (leaves.length === 1) return { root: leaves[0], proofs: [[]] };
|
||||
let layer = leaves.slice();
|
||||
const layers = [layer];
|
||||
while (layer.length > 1) {
|
||||
const next = [];
|
||||
for (let i = 0; i < layer.length; i += 2) {
|
||||
if (i + 1 === layer.length) next.push(layer[i]);
|
||||
else next.push(hashPair(layer[i], layer[i + 1]));
|
||||
}
|
||||
layer = next;
|
||||
layers.push(layer);
|
||||
}
|
||||
const root = layers[layers.length - 1][0];
|
||||
const proofs = leaves.map((_, idx) => {
|
||||
const proof = [];
|
||||
let index = idx;
|
||||
for (let l = 0; l < layers.length - 1; l++) {
|
||||
const cur = layers[l];
|
||||
const pair = index ^ 1;
|
||||
if (pair < cur.length) proof.push(cur[pair]);
|
||||
index = Math.floor(index / 2);
|
||||
}
|
||||
return proof;
|
||||
});
|
||||
return { root, proofs };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
G1, G2, Fr, DST,
|
||||
fpTo64Hex, g1Hex, g2Hex, randScalar, eip2537ToG1, eip2537ToG2,
|
||||
setupCommittee, epochIdentity, makeShare, reconstructDK,
|
||||
encrypt, decrypt, commitmentOf, leafOf, buildMerkle,
|
||||
};
|
||||
@ -7,6 +7,13 @@
|
||||
// - AereStakeQuotaPaymaster (stake AERE -> daily free-tx quota)
|
||||
// - AereAppPaymaster / Factory (per-dApp paymaster)
|
||||
//
|
||||
// STATUS OF THE `FINDING(...)` TESTS BELOW: see ../KNOWN-ISSUES.md. Both findings
|
||||
// affect V1 paymasters that are deployed but DEPRECATED and superseded by V2, and
|
||||
// both hold zero native balance as measured at head block 10,572,989. Read that
|
||||
// file before quoting a finding from this one. Note also that where a test group
|
||||
// below says "LIVE wiring", it means against real contract implementations rather
|
||||
// than mocks, on a local hardhat network. It does NOT mean against mainnet 2800.
|
||||
//
|
||||
// Tests run LOCAL (hardhat network) against the ACTUAL source (which matches the
|
||||
// deployed bytecode per the source-verification manifest). No source is modified
|
||||
// to make a test pass; failing tests that reflect real contract bugs are KEPT and
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
// shutter-mempool-v2.test.js
|
||||
//
|
||||
// Full PoC test for AereShutterMempoolV2: a REAL 3-of-5 threshold-BLS committee
|
||||
// generated by scripts/shutter-crypto.js (real ciphertexts, real shares, real
|
||||
// generated by test/helpers/shutter-crypto.js (real ciphertexts, real shares, real
|
||||
// Lagrange reconstruction) with every pairing verified on-chain via the local
|
||||
// prague EIP-2537 pairing precompile (0x0f), the same precompile probed live on
|
||||
// AERE chain 2800.
|
||||
const { expect } = require("chai");
|
||||
const { ethers } = require("hardhat");
|
||||
const crypto = require("crypto");
|
||||
const C = require("../scripts/shutter-crypto");
|
||||
const C = require("./helpers/shutter-crypto");
|
||||
|
||||
const T = 3, N = 5;
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// Audit-fix suite for AereShutterMempoolV3: reveal-timeout skip that restores
|
||||
// liveness. Follows the repo's V-reproduces / V-corrects pattern — every new
|
||||
// behaviour is shown STALLING on the live V2 design and RECOVERING on V3, using
|
||||
// the SAME real 3-of-5 threshold-BLS committee produced by scripts/shutter-crypto.js
|
||||
// the SAME real 3-of-5 threshold-BLS committee produced by test/helpers/shutter-crypto.js
|
||||
// (real ciphertexts, real shares, real Lagrange DK), all pairings verified
|
||||
// on-chain via the local prague EIP-2537 pairing precompile (0x0f), the same
|
||||
// precompile probed live on AERE chain 2800.
|
||||
@ -13,7 +13,7 @@ const { expect } = require("chai");
|
||||
const { ethers, network } = require("hardhat");
|
||||
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
|
||||
const crypto = require("crypto");
|
||||
const C = require("../scripts/shutter-crypto");
|
||||
const C = require("./helpers/shutter-crypto");
|
||||
|
||||
const T = 3, N = 5;
|
||||
const REVEAL_WINDOW = 3600; // seconds; short enough to fast-forward in tests
|
||||
|
||||
@ -19,7 +19,7 @@ describe("AereValidatorManifest", () => {
|
||||
DIGEST_A,
|
||||
"https://aere.network/.well-known/besu-image-digest",
|
||||
"AERE Foundation",
|
||||
"EU-Central / DE / example-dc-1"
|
||||
"EU-Central / DE"
|
||||
)
|
||||
).to.emit(ds, "CommitmentDeclared");
|
||||
const c = await ds.commitmentOf(val1.address);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user