321 lines
22 KiB
Markdown
321 lines
22 KiB
Markdown
# Post-quantum certificate anchor for QBFT
|
|
|
|
This is the code that puts a post-quantum validator certificate under the block hash in Hyperledger
|
|
Besu's QBFT consensus, as an overlay on a named upstream commit.
|
|
|
|
It is published so that the claim can be checked rather than believed. Everything below that is not
|
|
demonstrable from these files is marked as not demonstrable from these files.
|
|
|
|
---
|
|
|
|
## Scope boundary, stated first and not in a footnote
|
|
|
|
**Consensus on Aere Network chain 2800 is classical secp256k1 ECDSA QBFT with a post-quantum
|
|
checkpoint enforced every 32 blocks.** This code is what adds the checkpoint, in two steps that
|
|
should not be confused:
|
|
|
|
1. **The binding.** From block 13,014,000 the block hash of every anchored header (every 32nd
|
|
block) commits to a Falcon-512 certificate signed by the validators. That is a binding, not a
|
|
replacement: a rewrite of anchored history must also forge the post-quantum signatures.
|
|
2. **The enforced minimum.** Since 2026-08-14 a node does not finalize an anchor block unless
|
|
its certificate holds at least three valid Falcon-512 seals (f+1 of nine, the guarantee that at
|
|
least one honest validator signed; eight or nine are carried in practice). Raised at block
|
|
14,961,456 (August 21, 2026): the enforced minimum became **six**, which at that time was a full
|
|
2f+1 quorum of the nine-validator set, the same count QBFT itself required to agree on the block.
|
|
**Since 2026-09-11 the set is ten and QBFT's quorum is seven, so the same six is above f+1 and
|
|
below the quorum.** The threshold did not move; the set did. The schedule step is
|
|
`aere.pq.anchorMinSeals=13014000:0,13034000:3,14961456:6`, uniform on all ten validators; the
|
|
startup guard refuses any threshold above N - f = 7, the highest count reachable with the fault
|
|
budget down. Blocks between anchors carry no Falcon seals and are finalized by ECDSA alone, so
|
|
the post-quantum guarantee is a checkpoint about every 16 seconds, not a per-block quorum.
|
|
3. **The hybrid certificate.** Since block 17,047,600 (2026-09-04) the certificate is the v2, scheme-tagged
|
|
form `RLP[2, [[scheme, index, signature], ...]]` and carries Falcon-512 (lattice) AND SLH-DSA-SHA2-128s
|
|
(hash-based) seals; the enforced minimum applies to EACH scheme on its own, at least six of nine valid
|
|
seals of each, and every SLH-DSA seal must sit at an index that also carries a valid Falcon seal. Properties:
|
|
`aere.pq.anchorV2Block=17047600`, `aere.pq.hybrid.attachBlock=17047568`,
|
|
`aere.pq.schemeSchedule=17047568:falcon-512+slh-dsa-sha2-128s`, `aere.pq.hybridRegistry` (the epoch-2 key
|
|
manifest, also published under `/pq/manifests/`). The fleet schedule carries one dated step,
|
|
`17102384:0,17102416:6`: on 2026-09-04 a run of validator restarts emptied every node's seal store, the
|
|
chain stalled at anchor 17,102,384 for eleven minutes, and it was released by lowering the minimum to
|
|
zero at exactly that height and restoring six at the next anchor. The step stays in the schedule because
|
|
every node must agree on that history.
|
|
4. **The anchor interval.** `aere.pq.anchorIntervalSchedule=17225968:128`, set on all nine validators and
|
|
on every reading node on 2026-09-05: from block 17,225,968 an anchor falls on every 128th block instead
|
|
of every 32nd. Each hybrid certificate adds about 53 KB to its header; quartering the frequency divides
|
|
that cost by four without touching the rule, the thresholds or the schemes. Until that height the
|
|
interval is 32, and the checkpoint above becomes one about every 64 seconds after it.
|
|
|
|
Correction 2026-08-19: the fleet was also configured on 2026-08-15 with `aere.falcon.forkBlock`
|
|
at 14,050,000, intended as a per-block blocking Falcon quorum. In the shipped code that rule
|
|
(`FalconSealValidationRule`) is retired at `aere.pq.anchorBlock` (13,014,000) in favour of the
|
|
anchor rules (`PqAnchorConfig.legacyFalconRuleRetirementBlock`), so at 14,050,000 it was already
|
|
inert, and the chain itself confirms it: blocks between anchors carry no Falcon seals, which a
|
|
per-block quorum rule would have rejected. The earlier statement in this repository that from
|
|
14,050,000 every block required a 2f+1 Falcon quorum to finalize is withdrawn.
|
|
|
|
"Hybrid" is the honest word in two senses and no more: the checkpoint sense above and, since
|
|
2026-09-04, the two-scheme certificate. "Post-quantum consensus" without qualification still
|
|
overstates: per-block finality and validator identity are classical elliptic-curve cryptography. The
|
|
four QBFT message types carry a post-quantum seal of their author on chain 2800 since blocks
|
|
16,620,000 (PROPOSAL), 16,660,000 (ROUND-CHANGE) and 16,690,000 (PREPARE), COMMIT seals since the
|
|
anchor itself; and a message without a valid seal is REFUSED from blocks 17,250,000 (COMMIT),
|
|
17,400,000 (PROPOSAL), 17,550,000 (ROUND-CHANGE) and 17,700,000 (PREPARE). Those four enforcement
|
|
heights were set on all nine validators on 2026-09-05 and take effect as the chain reaches them;
|
|
until each height passes, that message type is still accepted without a seal. What a quantum
|
|
adversary cannot do at any height is rewrite history past the last anchor without also forging
|
|
Falcon-512 and SLH-DSA-SHA2-128s signatures. None of this has been audited by a third party.
|
|
|
|
The threat it addresses is not "harvest now, decrypt later". A signature is public; there is nothing
|
|
to harvest. The threat is **retroactive rewriting**: validator keys recovered later can be used to
|
|
re-sign old blocks, and a chain whose history is authenticated only by ECDSA cannot distinguish the
|
|
rewrite from the original. Binding a post-quantum certificate into the hash of every anchored header
|
|
means a rewrite must also forge the post-quantum signatures.
|
|
|
|
The threat is not ours to claim. It is the long-range attack, and the peer-reviewed treatment is
|
|
Azouvi, Danezis and Nikolaenko, "Winkle: Foiling Long-Range Attacks in Proof-of-Stake Systems"
|
|
(IACR 2019/1440; AFT 2020, pp. 189-201). Two things should be said plainly about that citation,
|
|
because we checked it rather than repeated it. **Winkle does not mention quantum adversaries at
|
|
all**: it treats the case where a validator's old signing keys reach an adversary, by any
|
|
means. The quantum framing is
|
|
ours. And **Winkle's own defence is not ours**: it adds a secondary layer of client-based
|
|
validation, where clients sign a hash of the previously sequenced block. A second published defence
|
|
for the same threat, Azouvi and Vukolic, "Pikachu: Securing PoS Blockchains from
|
|
Long-Range Attacks by Checkpointing into Bitcoin PoW using Taproot" (arXiv 2208.05408, 2022),
|
|
checkpoints into Bitcoin's proof of work.
|
|
|
|
### Where this sits among the known defences
|
|
|
|
The literature calls this attack **posterior corruption**, also known as the founders' attack, the
|
|
long-range attack, history revision, or costless simulation. Tas, Tse, Gai, Kannan, Maddah-Ali and
|
|
Yu survey the mitigations in "Bitcoin-Enhanced Proof-of-Stake Security: Possibilities and
|
|
Impossibilities" (arXiv 2207.08392) and count four: social consensus by a trusted committee;
|
|
key-evolving signatures, where validators forget their old keys; verifiable delay functions; and
|
|
timestamping onto an existing proof-of-work chain.
|
|
|
|
That paper also proves an impossibility, and it deserves to be stated correctly rather than
|
|
paraphrased into something scarier or softer than it is. **Slashable safety**, meaning safety plus the
|
|
ability to punish the attacker, is not achievable by a proof-of-stake chain without an external
|
|
trusted source. Plain safety against posterior corruption is a weaker property, and key-evolving
|
|
signatures are shown to provide it under an honest majority. So the impossibility is not "you cannot
|
|
defend yourself"; it is "you cannot make the attacker pay, on your own."
|
|
|
|
**This anchor does not claim slashable safety and does not compete with checkpointing on that
|
|
ground.** What it addresses is the case where key-evolving signatures stop working. Their guarantee
|
|
rests on a validator's old key being *gone* once deleted. Against an adversary with a
|
|
cryptographically relevant quantum computer, an old secp256k1 key does not need to have been kept or
|
|
stolen. QBFT headers carry validator addresses rather than public keys, but every seal a validator
|
|
ever wrote is an ECDSA signature from which its public key can be recovered, and those seals are in
|
|
the chain forever. From the public key, a quantum adversary derives the private one. Deleting the
|
|
private key buys nothing, because the chain reconstructs it. The certificate under the block hash is what still has to be forged, and forging it requires
|
|
a secret the adversary cannot derive.
|
|
|
|
Put plainly: this is the post-quantum analogue of what key-evolving signatures give classically, for
|
|
the setting in which their central assumption no longer holds. It is narrower than checkpointing and
|
|
it borrows nothing from another chain, and both of those are true at the same time.
|
|
|
|
---
|
|
|
|
## The problem this solves, which is not obvious
|
|
|
|
In QBFT, `extraData` is an RLP list. The block hash is **not** computed over the stored bytes. It is
|
|
computed over a re-encoding of the decoded list, with the seal fields removed, so that every node
|
|
agrees on a hash before the seals exist.
|
|
|
|
That has a consequence that is easy to miss: **anything the decoder does not know about is dropped
|
|
before hashing.** Append a certificate as a new element and it survives in storage, travels between
|
|
nodes, and is entirely absent from the hash. Two nodes can hold different certificates for the same
|
|
block and both consider it valid. The certificate would be decoration.
|
|
|
|
Four obvious designs were tried and all four died on that:
|
|
|
|
| Attempt | Why it died |
|
|
|---|---|
|
|
| new RLP element after the seals | dropped by the re-encode, never reaches keccak |
|
|
| extend the seal list | changes the seal encoding, so every existing node rejects the header |
|
|
| a second `extraData`-like field | not in the header schema; a header with it is not a header |
|
|
| commit in the state root | the state root is computed before the certificate exists |
|
|
|
|
The design that works uses a field that is **already under keccak**: `vanityData`, element 0 of the
|
|
list, 32 bytes, historically arbitrary. At an anchor height it carries the digest of the certificate
|
|
instead of the usual vanity string. The certificate itself still rides outside the hash, but it is
|
|
now pinned: change one byte of it and the digest no longer matches, and the header is rejected.
|
|
|
|
`PqAnchor.java` and `PqAnchorDigestRule.java` are where that lives. Read those two first.
|
|
|
|
---
|
|
|
|
## What is in here
|
|
|
|
Updated 2026-08-29. The first publication of this directory, on 2026-08-11, was staged by hand and
|
|
was never refreshed: by 29 August, 63 of the 78 files here had changed in the tree we actually run
|
|
and 50 files had never been published at all. That is fixed at the root rather than by one copy —
|
|
`aerenew/publish-bundle/stage-anchor.cjs` in the operator repository derives this directory from the authoritative overlay, refuses
|
|
to run if the two files this README tells you to read first are missing, deletes what we removed
|
|
rather than letting the published set grow monotonically, and stops on the secret-scanner's verdict
|
|
instead of around it. The per-file SHA-256 list is in `MANIFEST-sha256.txt`.
|
|
|
|
**Three of the files in the overlay are not here, and it is the same three every time**: the
|
|
negative-control harnesses. They plant a defect on purpose to prove a guard can fail, so they are a
|
|
recipe for disabling a guard rather than a description of one. Everything they prove is stated in
|
|
"What is proven, and by what" below, and every guard they exercise is here.
|
|
|
|
### What is armed on chain 2800, and what is only present
|
|
|
|
This matters more than the file list, so it is stated before it.
|
|
|
|
| Layer | In this directory | Armed on chain 2800 |
|
|
|---|---|---|
|
|
| Anchor certificate under the block hash | yes | **yes**, since block 13,014,000 |
|
|
| Enforced minimum seals at an anchor height | yes | **yes**, 6 of 9 since 14,961,456 |
|
|
| Legacy per-block Falcon rule (`aere.falcon.forkBlock`) | yes | **no** — retired at the anchor block |
|
|
| Post-quantum seal on PREPARE, emission | yes | **yes**, since block 16,690,000 |
|
|
| Post-quantum seal on PREPARE, enforcement | yes | **armed for block 17,700,000** (set 2026-09-05) |
|
|
| Post-quantum seal on the PROPOSAL, emission | yes | **yes**, since block 16,620,000 |
|
|
| Post-quantum seal on the PROPOSAL, enforcement | yes | **armed for block 17,400,000** (set 2026-09-05) |
|
|
| Post-quantum seal on ROUND-CHANGE, emission / enforcement | yes | **yes**, since block 16,660,000 / **armed for block 17,550,000** |
|
|
| Post-quantum seal on COMMIT, emission / enforcement | yes | **yes**, since the anchor / **armed for block 17,250,000** |
|
|
| Hybrid Falcon + SLH-DSA certificate (v2) | yes | **yes**, since block 17,047,600 (2026-09-04) |
|
|
| Anchor interval 128 (`aere.pq.anchorIntervalSchedule`) | yes | **armed for block 17,225,968** (set 2026-09-05) |
|
|
|
|
Everything not yet in force is **disarmed by absence or by height, never by a flag**: the properties that arm them
|
|
are unset, and unset means never. Each refuses loudly on a value it cannot parse rather than booting
|
|
a node that believes itself armed — a node that disarms itself because of a mistyped character looks
|
|
exactly like a correctly configured one, right up to the day it matters. The tests for that
|
|
behaviour are in this directory and they are the ones to read if you doubt the claim.
|
|
|
|
The PREPARE layer is newer than the anchor and stronger where it applies: an armed node that refuses
|
|
unsealed PREPAREs never reaches the prepared state, so it never sends COMMIT at all. That also means
|
|
it has no safety net during an activation, which is why it was armed LAST and highest: its
|
|
enforcement height on chain 2800 is 17,700,000, set on 2026-09-05 after the COMMIT, PROPOSAL and
|
|
ROUND-CHANGE heights, so that the three other layers are in force before it.
|
|
|
|
The PROPOSAL layer (added 2026-08-30) closes the remaining hot-path gap, and its claim is stated
|
|
precisely because the imprecise version oversells: an adversary who breaks ECDSA cannot finalize
|
|
anything while the PREPARE layer is armed, since "prepared" needs a full quorum of PREPAREs. What
|
|
forged proposals could still do is OPEN rounds and waste them. With proposal enforcement armed, a
|
|
proposal without a valid Falcon seal from its own proposer does not open a round. The seal signs
|
|
its own domain over (chainId, height, round, digest), so an honest proposal seal cannot be replayed
|
|
as a vote nor a vote seal as a proposal - both directions are tested. The ROUND-CHANGE message
|
|
carries its own seal since 2026-08-31 (its embedded justifications are coupled to PREPARE
|
|
enforcement). What remains classical: node-level devp2p authentication and validator identity.
|
|
|
|
### The files
|
|
|
|
- `consensus/common/.../bft/` — the anchor itself: configuration, the digest, the seal cache and
|
|
store, the producer that attaches seals, the Falcon registry that maps a validator to a key.
|
|
- `consensus/qbft/.../headervalidationrules/` — the validation rules: the digest must match, the
|
|
seals must verify, and the rules must actually be wired into the validator chain.
|
|
- `app/.../controller/` — where the rules are built and where the node refuses to start on a
|
|
configuration that would produce headers its own fleet rejects.
|
|
- tests — including the negative controls. A test that cannot fail is not a test, and several of
|
|
these exist specifically to prove the guards can fail.
|
|
|
|
`BASE.txt` names the upstream commit. Applying these files to any other tree overwrites whatever
|
|
upstream added since, silently. That is stated there in more detail because it is a real hazard.
|
|
|
|
**One build file changes, and it is one line.** `consensus/common/build.gradle` gains
|
|
`implementation 'org.bouncycastle:bcprov-jdk18on'`. Falcon verification happens inside the consensus
|
|
module, so the module needs the library on its own compile path. No version is stated, because
|
|
upstream pins it: `platform/build.gradle` declares `bcprov-jdk18on:1.83`, and four upstream modules
|
|
already take the dependency in exactly this versionless form. So the line adds a compile-path entry
|
|
and no new artifact, and it does not move any version. That is the entire build change, and it is
|
|
called out here rather than left to be found in the diff.
|
|
|
|
Falcon signature verification calls Bouncy Castle's implementation. SLH-DSA-SHA2-128s signing and
|
|
verification use the `slhdsa/` package in `consensus/common`: Bouncy Castle 1.80's own SLH-DSA
|
|
implementation, copied into the tree with one change, the SHA-256 and SHA-512 digests come from the
|
|
JDK (`MessageDigest`, hardware SHA extensions) instead of Bouncy Castle's pure-Java digests, because
|
|
the pure-Java path made one anchor signature cost 1.4 to 2.7 seconds on the consensus thread
|
|
(measured 2026-09-04). Signatures are byte-identical to the original in deterministic mode, which
|
|
`SlhDsaFastEngineTest` pins with cross-verification in both directions. The package is regenerated
|
|
from the published Bouncy Castle sources by a script, not edited by hand, and carries the Bouncy
|
|
Castle licence next to it (`consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/LICENSE-BouncyCastle.txt`). What is ours is the framing, the
|
|
registry that maps a validator to its keys, the digest, the scheme schedule and the validation rules.
|
|
|
|
---
|
|
|
|
## What is proven, and by what
|
|
|
|
- **The certificate is byte-identical across nodes.** Measured on a test network, six nodes.
|
|
- **A stripped certificate is rejected.** 12 attempts, 12 rejections.
|
|
- **The guards compile and pass together**, and the negative control was run: with the guards
|
|
removed the proofs go red. A guard that has never failed cannot be trusted, so each was made to
|
|
fail on purpose.
|
|
- **Arming does not halt the chain across a validator-set change.** On a test network of ten
|
|
processes, 1,110 blocks were produced across the arming height while four validator-set votes were
|
|
driven through it, and the block rate did not change. The negative control for that run was
|
|
separate and blunt: three nodes restarted without their Falcon key produced zero blocks in ninety
|
|
seconds while every node was alive; with the keys restored, eighty-nine.
|
|
- **The wiring is tested.** An earlier version of this code registered a rule that could not be seen
|
|
from outside, because Besu's `BlockHeaderValidator.Builder` wraps detached rules in a lambda. The
|
|
rule was present and untested for that reason alone. `QbftAnchorRuleWiringTest` exists because of
|
|
that, and its negative control is measured: comment out the registration line and it goes red.
|
|
|
|
## What is not proven here
|
|
|
|
- **This overlay has not been audited by a third party.** No external security review of this code
|
|
exists. If you are reading it as an auditor, you are the first.
|
|
- **A rehearsal with a deliberately un-upgraded node has not been run.** Every rehearsal so far
|
|
upgraded every node.
|
|
- **Nothing here demonstrates what is configured on any live network.** These files show what the
|
|
code does when armed. They are not evidence about any running fleet, and should not be read as any.
|
|
- **The message-layer ENFORCEMENT has not yet refused anything on chain 2800.** Emission runs on the
|
|
live fleet; enforcement is armed by height (see the table) and, at the time of writing, none of the
|
|
four heights has passed. The refusal path has been exercised on the public testnet 28001, including
|
|
against a second, independent client implementation; test-network evidence is evidence about a test
|
|
network.
|
|
- **The hybrid certificate is live but young.** Hybrid certificates have been produced on chain 2800
|
|
since 2026-09-04 with real validator keys, and the public verifier reproduces them from the
|
|
internet. What is not shown here is any third-party review of the SLH-DSA engine copy or of the
|
|
scheme layer.
|
|
|
|
## One claim we retracted, on purpose
|
|
|
|
An earlier version of our public material said that no public chain has a block hash covering a
|
|
post-quantum validator certificate. That does not survive a hostile reading. Cellframe's ESBoCS
|
|
signs blocks with keys that resolve to Dilithium, Falcon or SPHINCS+, and hashes the block with the
|
|
signatures attached. The capability exists in their code today.
|
|
|
|
The defensible statement is narrower: no public chain has a post-quantum validator certificate under
|
|
the block hash **that is proven and independently verifiable**. This repository is our half of that
|
|
sentence. Someone else has to do the verifying, which is why it is here.
|
|
|
|
---
|
|
|
|
## Terminology, used precisely
|
|
|
|
**A certificate at an anchor height is signed by at least six validators, since block 14,961,456
|
|
(August 21, 2026). It was f+1 before that, and this paragraph said so. Between that date and
|
|
2026-09-11 six WAS the 2f+1 quorum of a nine-validator set; since the tenth validator joined, the
|
|
quorum is seven and six is not one, so we do not call it a quorum.** The distinction
|
|
is worth keeping in view rather than deleting, because it is the difference between two genuinely
|
|
different claims. f+1 signatures guarantee only that at least one honest validator signed. A quorum
|
|
of 2f+1 is the same count QBFT itself requires to agree on the block. Between 14,961,456 and
|
|
2026-09-11 the post-quantum certificate at an anchor height was backed by as many validators as the
|
|
block itself; with ten validators it is backed by six of the seven the block needs, which is more
|
|
than f+1 and less than the block's own quorum. Raising it to seven is a deliberate decision with a
|
|
liveness cost, and it has not been taken.
|
|
|
|
Two things follow, and both should be said rather than left to be discovered. The word "quorum"
|
|
applies **only at anchor heights** - about every 32nd block; the blocks between them carry no Falcon
|
|
seals at all and are finalized by ECDSA alone, so "a post-quantum quorum on every block" would be
|
|
false. And the higher threshold costs liveness margin: at K=6 of ten, four late validators are
|
|
enough to stall an anchor block, where K=3 tolerated seven. That trade was made deliberately, and it
|
|
is the reason the threshold is a configured schedule rather than a constant.
|
|
|
|
Anyone counting will notice which of the two regimes a given height falls in, and they should. The
|
|
schedule is in the header of this document and on every node.
|
|
|
|
The seal threshold is a **floor, not a cap**: nodes attach as many verified seals as arrive in time,
|
|
which is at least the threshold and often more. A separate cap bounds how many are written, because
|
|
each seal costs bytes in every header forever.
|
|
|
|
---
|
|
|
|
## Licence
|
|
|
|
Apache 2.0, matching upstream Hyperledger Besu. See `../LICENSE` and `../NOTICE`. Files that modify
|
|
upstream carry the change notice required by section 4(b); files that are new are ours. The `slhdsa/`
|
|
package in `consensus/common` is derived from Bouncy Castle and stays under the Bouncy Castle licence
|
|
(MIT-style); its notice is kept next to the code in `LICENSE-BouncyCastle.txt`.
|