Patch 0003 regenerated with the schedule-boundary fix: a package-built node now crosses the first real anchor

The published patch carried a one-block boundary defect: the seal rule asks the
index-to-address map at the parent height (H-1) while the registry schedule
binds inclusively from H, so at the very first anchor the schedule was empty and
the lookup fell back to a head map that is empty on a node synced from genesis.
A valid certificate was rejected. Fixed in both lookup paths: when the schedule
has an entry at exactly blockNumber+1, answer from that entry's verified bound
registry. The boundary stays exactly one block.

Proven by import, not by assertion: the package-built node was stuck at
13,013,999 with 6,646 rejections; with the fix it crossed 13,014,000 in 42
seconds and imported 253,729 further blocks through the anchor era with zero
rejections. Tests: new PqParentHeightAlignmentTest 3/3, consensus:common 391/0,
consensus:qbft 217/0. Negative control measured: disarm the condition and the
measuring test goes red; restore it and it is green.

Patch verified to apply cleanly on a pristine upstream checkout, alone and in
series with 0001/0004/0005; file list is the previous 75 plus the new test, and
every untouched section is byte-identical to the old patch. The anchor/ source
copy in this package was brought to the same state so the two cannot diverge.

Honest limit, recorded in IMPORT-PROOF-STARE: further along, at 13,267,729, the
node stops again for a DIFFERENT reason. Blocks in a band there carry an
attached certificate whose vanityData is the ordinary client string rather than
the digest, at heights a single static interval treats as anchor heights. The
best-supported reading is that the fleet ran a different anchoring configuration
in that window (these values are not consensus-bound), so the package needs a
HISTORICAL anchoring schedule, not one value. Stated rather than implied.
This commit is contained in:
Aere Network 2026-08-16 00:03:20 +03:00
parent 85747e23af
commit 67e3c26e1c
4 changed files with 597 additions and 9 deletions

View File

@ -38,3 +38,33 @@ carpeala in focul serii.
**RUN-A-NODE.md poarta deja avertismentul corect:** aplica 0001+0003+0004+0005 si trateaza **RUN-A-NODE.md poarta deja avertismentul corect:** aplica 0001+0003+0004+0005 si trateaza
build-ul ca urmaritor pana la o dovada de import completa. Aceasta dovada de import a ajuns pana build-ul ca urmaritor pana la o dovada de import completa. Aceasta dovada de import a ajuns pana
la prima ancora si a gasit exact veriga care lipseste. Constatarea intra in registru. la prima ancora si a gasit exact veriga care lipseste. Constatarea intra in registru.
## Adus la zi 15 august, noaptea
- **Cauza, gasita si dovedita in aceeasi seara:** un decalaj de un bloc la granita orarului.
Regula sigiliilor judeca certificatul purtat de blocul primei ancore H intreband registrul la
inaltimea PARINTELUI, H-1, fiindca aceea e inaltimea peste care se semneaza sigiliile. Orarul
registrelor leaga insa INCLUSIV de la H, deci la H-1 nu leaga nimic, si ambele cautari pe
inaltime cadeau pe registrul de cap, care pe un nod sincronizat de la geneza e gol. Nu era o
divergenta de continut a maparii index catre adresa, cum banuia sectiunea de mai sus: era
intrebarea pusa cu un bloc mai jos decat singura intrare care putea raspunde.
- **Reparatia minima, in FalconSealSupport, pe amandoua drumurile (cheie si adresa):** cand
orarul are o intrare la EXACT blockNumber+1, raspunsul vine din registrul legat si verificat
al acelei intrari, nu din fisierul de cap neverificat. Granita e exact un bloc: cu doua sau
mai multe blocuri sub orar nu se schimba nimic.
- **Dovada, masurata, nu presupusa:** nodul construit din pachete a trecut de 13.014.000 si a
mers peste 25.000 de blocuri fara nicio respingere, trecand si de treapta cu prag 3 de la
13.034.000. Suitele modulelor de consens: 608 teste, zero esecuri, inclusiv testul nou
PqParentHeightAlignmentTest, construit pe forma nodului public (cap gol, registrul doar ca
istorie). Control negativ facut: cu conditia dezarmata, exact proba granitei iese rosie; cu
ea la loc, verde.
- **Peticul 0003 publicat poarta acum reparatia**, regenerat din amonte pristin, cu aceeasi
lista de fisiere plus testul nou, si se aplica curat in seria 0001, 0003, 0004, 0005.
Avertismentul din RUN-A-NODE.md ramane pana la o dovada de import completa pana la varf.
- **Unde a ajuns rularea dupa reparatie, scris cinstit:** nodul a mers de la 13.014.000 pana la
13.267.729, adica 253.729 de blocuri prin era ancorei, cu zero respingeri de certificat, si
s-a oprit acolo la o granita NOUA, separata de cea reparata: blocuri din banda urmatoare
poarta certificat atasat dar vanityData e sirul obisnuit al clientului, nu digestul, la
inaltimi pe care configuratia statica a nodului le considera inaltimi de ancora, si regula
digestului le refuza. Productia a acceptat acele blocuri la vremea lor, deci pachetul public
inca nu reproduce si acest interval; e urmatoarea veriga de investigat, cu aceeasi metoda.

View File

@ -3442,6 +3442,25 @@ public final class FalconSealSupport {
final Optional<PqRegistryHash.ScheduleEntry> required = final Optional<PqRegistryHash.ScheduleEntry> required =
PqRegistryHash.requiredHashAt(schedule, blockNumber); PqRegistryHash.requiredHashAt(schedule, blockNumber);
if (required.isEmpty()) { if (required.isEmpty()) {
// SCHEDULE BOUNDARY (2026-08-15). The only historical question ever asked one block
// below the schedule's first entry is about the certificate carried by the block at
// blockNumber+1, whose governing registry is the one bound EXACTLY at blockNumber+1.
// Answer it from that entry's VERIFIED bound registry, never from the unverified head
// file. Exactly one block: at blockNumber+2 below the schedule nothing changes.
final Optional<PqRegistryHash.ScheduleEntry> nextEntry =
PqRegistryHash.requiredHashAt(schedule, blockNumber + 1);
if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) {
final Optional<PqRegistryHash.Registry> boundNext =
PqRegistryHash.registryAt(schedule, set, blockNumber + 1);
if (boundNext.isPresent()) {
for (final PqRegistryHash.Entry e : boundNext.get().entries()) {
if (e.index() == validatorIndex) {
return new FalconPublicKeyParameters(FalconParameters.falcon_512, e.publicKey());
}
}
return null;
}
}
return headRegistryKeyOrRefuse( return headRegistryKeyOrRefuse(
blockNumber, validatorIndex, "the schedule binds no registry at this height", historic); blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
} }
@ -3493,6 +3512,27 @@ public final class FalconSealSupport {
blockNumber, validatorIndex, "no schedule was ever loaded", historic); blockNumber, validatorIndex, "no schedule was ever loaded", historic);
} }
if (PqRegistryHash.requiredHashAt(schedule, blockNumber).isEmpty()) { if (PqRegistryHash.requiredHashAt(schedule, blockNumber).isEmpty()) {
// SCHEDULE BOUNDARY (2026-08-15). Same alignment as in keyAt: one block below the
// schedule's first entry the subject is the certificate of the block at blockNumber+1,
// governed by the registry bound EXACTLY there, so the answer comes from that entry's
// VERIFIED bound registry and mirrors what this method answers at blockNumber+1 itself.
final Optional<PqRegistryHash.ScheduleEntry> nextEntry =
PqRegistryHash.requiredHashAt(schedule, blockNumber + 1);
if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) {
final Optional<PqRegistryHash.Registry> boundNext =
PqRegistryHash.registryAt(schedule, set, blockNumber + 1);
if (boundNext.isPresent()) {
if (!boundNext.get().addressBound()) {
return null;
}
for (final PqRegistryHash.Entry e : boundNext.get().entries()) {
if (e.index() == validatorIndex && e.address() != null) {
return Address.wrap(Bytes.wrap(e.address()));
}
}
return null;
}
}
return headRegistryAddressOrRefuse( return headRegistryAddressOrRefuse(
blockNumber, validatorIndex, "the schedule binds no registry at this height", historic); blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
} }

View File

@ -0,0 +1,229 @@
/*
* Copyright contributors to Besu / AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.consensus.common.bft;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
import org.hyperledger.besu.datatypes.Address;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.bouncycastle.crypto.digests.KeccakDigest;
import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
* SCHEDULE BOUNDARY (2026-08-15): the PARENT-HEIGHT question at the first anchor.
*
* <p>WHAT WAS MEASURED, on a public node synced from genesis on the live chain. {@code
* PqAnchorSealsRule} judges the certificate carried by the block at the first anchor height H by
* asking the registry at the PARENT's height H-1, because that is the height the seals commit to.
* The schedule binds INCLUSIVELY from H, so at H-1 it binds nothing, and both height-resolved
* lookups fell back to the HEAD registry - which on a public node synced from genesis is EMPTY. A
* perfectly valid certificate was refused with "certificate carries index 0, which the registry
* does not bind to any validator address", and the node parked one block below the first anchor
* forever (measured: head 13,013,999 = H-1, 6,646 R2 rejections in the log).
*
* <p>THE REPAIR UNDER TEST. In the branch where {@code requiredHashAt(schedule, blockNumber)} is
* empty, before the head fallback: when the schedule has an entry at EXACTLY {@code blockNumber+1},
* answer from that entry's VERIFIED bound registry. The only historical question ever asked at H-1
* is about the certificate carried by the block at H, whose governing registry is the one bound at
* H; the alignment uses the verified v2 registry, not an unverified head file.
*
* <p>THE FIXTURE IS THE PUBLIC-NODE SHAPE, DELIBERATELY: no {@code aere.falcon.genesis}, so the
* head registry is EMPTY, and the registry reaches the node ONLY as history ({@code
* aere.falcon.registry.history}) bound by hash to the schedule entry at H. That emptiness is what
* makes the negative control possible - with a populated head, the H-1 assertions would pass
* through the old head fallback and prove nothing about the repair. {@link
* #baselineTheHeadIsEmptyOtherwiseNothingHereMeansAnything()} pins that shape.
*/
public class PqParentHeightAlignmentTest {
/** Anchor activation height H: the schedule's FIRST entry sits exactly here. */
private static final long H = 1_000L;
/** Height from which the staged threshold is non-zero, i.e. the fully armed regime. */
private static final long K_AT = H + 10L;
private static final int N = 7;
private static final long CHAIN_ID = 220_878L;
@TempDir private Path tmp;
private final List<FalconPrivateKeyParameters> privateKeys = new ArrayList<>();
private final List<Address> validators = new ArrayList<>();
private Path genesisPath;
/** A fixed 32-byte message, standing in for M(parent) of the first anchor block. */
private static final Bytes32 MESSAGE = Bytes32.fromHexString("0x" + "5a".repeat(32));
private Bytes sealByIndexZero;
@BeforeEach
public void setUp() throws Exception {
// The same v2 proof-bound manifest as PqRegistryHeightRefusalTest, bound at H.
final KeccakDigest kd = new KeccakDigest(256);
final StringBuilder manifest = new StringBuilder();
manifest
.append("{\"config\":{\"aereFalconRegistry\":{")
.append(PqV2Fixture.manifestHeader(N, CHAIN_ID, H));
for (int i = 0; i < N; i++) {
privateKeys.add(PqV2Fixture.privateKey(i));
validators.add(PqV2Fixture.address(i));
final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i);
kd.update(anchoredRow, 0, anchoredRow.length);
manifest.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, H));
}
manifest
.append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x")
.append("0".repeat(64))
.append("\":\"0x");
final byte[] anchoredHash = new byte[32];
kd.doFinal(anchoredHash, 0);
manifest.append(Bytes.wrap(anchoredHash).toUnprefixedHexString()).append("\"}}}}");
genesisPath = tmp.resolve("genesis-d228.json");
Files.writeString(genesisPath, manifest.toString());
// DELIBERATELY NOT setting aere.falcon.genesis: the head registry stays EMPTY, which is the
// public-node shape the defect was measured on.
// A genuine Falcon-512 signature by index 0 over MESSAGE.
final FalconSigner signer = new FalconSigner();
signer.init(true, privateKeys.get(0));
sealByIndexZero = Bytes.wrap(signer.generateSignature(MESSAGE.toArray()));
resetFalconSingleton();
// ARMED at H, exactly like the live fleet at its first anchor height.
PqAnchorProducer.useConfigForTesting(
new PqAnchorConfig(CHAIN_ID, H, Map.of(H, 0, K_AT, 3), OptionalInt.empty(), false));
// The registry reaches this node ONLY as history, exactly like the public package does it.
System.setProperty(
FalconSealSupport.PROPERTY_REGISTRY_HISTORY, genesisPath.toAbsolutePath().toString());
final FalconSealSupport pqc = FalconSealSupport.instance();
final PqRegistryHash.Registry held = PqRegistryHash.loadAuto(genesisPath);
final PqRegistryHash.Schedule schedule =
scheduleFromGenesis(Map.of(H, PqRegistryHash.hashFor(held, CHAIN_ID)));
pqc.verifyRegistryBindingOrAbort(0L, CHAIN_ID, schedule);
}
@AfterEach
public void tearDown() throws Exception {
System.clearProperty("aere.falcon.genesis");
System.clearProperty("aere.pq.genesis");
System.clearProperty(FalconSealSupport.PROPERTY_REGISTRY_HISTORY);
resetFalconSingleton();
PqAnchorProducer.useConfigForTesting(null);
}
// -------------------------------------------------------------------------------------------
// 0. The fixture itself: the head must be EMPTY, or the two tests below pass through the old
// head fallback and the negative control cannot go red.
// -------------------------------------------------------------------------------------------
@Test
public void baselineTheHeadIsEmptyOtherwiseNothingHereMeansAnything() {
final FalconSealSupport pqc = FalconSealSupport.instance();
assertThat(pqc.addressForIndex(0))
.describedAs("the HEAD registry must be empty: this is the public-node shape")
.isNull();
assertThat(pqc.verify(0, MESSAGE, sealByIndexZero))
.describedAs("a head verify must fail for the same reason")
.isFalse();
}
// -------------------------------------------------------------------------------------------
// 1. THE MEASUREMENT. At H-1, the question is about the certificate of the block at H, and the
// registry bound at H answers it.
// -------------------------------------------------------------------------------------------
@Test
public void atTheParentOfTheFirstAnchorTheBoundRegistryAnswers() {
final FalconSealSupport pqc = FalconSealSupport.instance();
assertThat(pqc.addressForIndexAtHistoric(H - 1L, 0))
.describedAs(
"SCHEDULE BOUNDARY: PqAnchorSealsRule asks at the PARENT height H-1 about the certificate carried "
+ "by the block at H. The registry governing that certificate is the one bound at "
+ "H, and it is VERIFIED; refusing here parks a syncing node at H-1 forever")
.isEqualTo(validators.get(0));
assertThat(pqc.verifyAtHistoric(H - 1L, 0, MESSAGE, sealByIndexZero))
.describedAs("and the key half must answer from the same bound registry")
.isTrue();
}
// -------------------------------------------------------------------------------------------
// 2. THE BOUNDARY IS EXACTLY ONE BLOCK. At H-2 and below, nothing changes: the head fallback
// still answers, and on this empty head that answer is a refusal, exactly as before.
// -------------------------------------------------------------------------------------------
@Test
public void theBoundaryIsExactlyOneBlock() {
final FalconSealSupport pqc = FalconSealSupport.instance();
assertThat(pqc.addressForIndexAtHistoric(H - 2L, 0))
.describedAs("H-2 is not the parent of any anchor-governed height: old behaviour, refusal")
.isNull();
assertThat(pqc.verifyAtHistoric(H - 2L, 0, MESSAGE, sealByIndexZero)).isFalse();
assertThat(pqc.addressForIndexAtHistoric(0L, 0))
.describedAs("and deep below, the same")
.isNull();
}
// -------------------------------------------------------------------------------------------
// Helpers, borrowed from PqRegistryHeightRefusalTest.
// -------------------------------------------------------------------------------------------
private PqRegistryHash.Schedule scheduleFromGenesis(final Map<Long, String> entries)
throws Exception {
final List<Long> heights = new ArrayList<>(entries.keySet());
heights.sort(Long::compare);
final StringBuilder sb = new StringBuilder("{\"config\":{\"pqRegistryHash\":[");
for (int i = 0; i < heights.size(); i++) {
if (i > 0) {
sb.append(',');
}
final long b = heights.get(i);
String h = entries.get(b);
if (!h.startsWith("0x")) {
h = "0x" + h;
}
sb.append("{\"block\":").append(b).append(",\"hash\":\"").append(h).append("\"}");
}
sb.append("]}}");
final Path p =
tmp.resolve("genesis-schedule-" + heights.size() + "-" + heights.get(0) + ".json");
Files.writeString(p, sb.toString());
return PqRegistryHash.loadScheduleFromGenesis(p);
}
private static void resetFalconSingleton() throws Exception {
final Field f = FalconSealSupport.class.getDeclaredField("instance");
f.setAccessible(true);
f.set(null, null);
}
}

View File

@ -1,6 +1,6 @@
From 050489f7245fd9e1fa982e844b6984a3eba45d29 Mon Sep 17 00:00:00 2001 From ce832eb0614c6029d1aaa503b3717a83b6d02734 Mon Sep 17 00:00:00 2001
From: Aere Network <node@aere.network> From: Aere Network <node@aere.network>
Date: Wed, 12 Aug 2026 02:25:12 +0300 Date: Sat, 15 Aug 2026 23:43:50 +0300
Subject: [PATCH] Aere Network: post-quantum certificate anchor for QBFT Subject: [PATCH] Aere Network: post-quantum certificate anchor for QBFT
Puts a 32-byte digest of the validator certificate into vanityData, which is Puts a 32-byte digest of the validator certificate into vanityData, which is
@ -15,6 +15,18 @@ same Apache License 2.0; the upstream copyright headers are left as found.
Consensus on chain 2800 remains classical secp256k1 ECDSA. This binds a Consensus on chain 2800 remains classical secp256k1 ECDSA. This binds a
post-quantum certificate to the block hash; it does not make consensus post-quantum certificate to the block hash; it does not make consensus
post-quantum. post-quantum.
Updated 2026-08-15: repairs the schedule boundary at the first anchor height.
The seals rule resolves the certificate carried by the block at height H
against the registry at the parent height H-1, and the height-to-registry
schedule binds inclusively from H, so at H-1 both height-resolved lookups fell
back to the head registry, which is empty on a node syncing from genesis, and
a valid certificate was refused one block below the first anchor. Both lookups
now answer that one question from the schedule entry bound exactly one block
above, using the verified registry; two or more blocks below the schedule
nothing changes. Covered by the new PqParentHeightAlignmentTest, and measured
on a node synced from genesis, which now crosses the first anchor height
instead of stopping one block below it.
--- ---
.../org/hyperledger/besu/cli/BesuCommand.java | 24 + .../org/hyperledger/besu/cli/BesuCommand.java | 24 +
.../cli/options/AerePqEmergencyOptions.java | 245 ++ .../cli/options/AerePqEmergencyOptions.java | 245 ++
@ -25,14 +37,14 @@ post-quantum.
.../common/bft/BftBlockInterface.java | 22 +- .../common/bft/BftBlockInterface.java | 22 +-
.../consensus/common/bft/BftExtraData.java | 50 +- .../consensus/common/bft/BftExtraData.java | 50 +-
.../besu/consensus/common/bft/FalconSeal.java | 88 + .../besu/consensus/common/bft/FalconSeal.java | 88 +
.../common/bft/FalconSealSupport.java | 3709 +++++++++++++++++ .../common/bft/FalconSealSupport.java | 3749 +++++++++++++++++
.../besu/consensus/common/bft/PqAnchor.java | 335 ++ .../besu/consensus/common/bft/PqAnchor.java | 335 ++
.../consensus/common/bft/PqAnchorConfig.java | 1320 ++++++ .../consensus/common/bft/PqAnchorConfig.java | 1320 ++++++
.../common/bft/PqAnchorNotReadyException.java | 132 + .../common/bft/PqAnchorNotReadyException.java | 132 +
.../common/bft/PqAnchorSyncModeGuard.java | 130 + .../common/bft/PqAnchorSyncModeGuard.java | 130 +
.../common/bft/PqAnchorThresholdGuard.java | 252 ++ .../common/bft/PqAnchorThresholdGuard.java | 252 ++
.../common/bft/PqRegistryBinding.java | 497 +++ .../common/bft/PqRegistryBinding.java | 497 +++
.../consensus/common/bft/PqRegistryHash.java | 2341 +++++++++++ .../consensus/common/bft/PqRegistryHash.java | 2341 ++++++++++
.../common/bft/PqRegistryHashTool.java | 482 +++ .../common/bft/PqRegistryHashTool.java | 482 +++
.../consensus/common/bft/PqSealCache.java | 351 ++ .../consensus/common/bft/PqSealCache.java | 351 ++
.../consensus/common/bft/PqSealStore.java | 384 ++ .../consensus/common/bft/PqSealStore.java | 384 ++
@ -40,7 +52,7 @@ post-quantum.
.../blockcreation/BftBlockCreatorFactory.java | 25 +- .../blockcreation/BftBlockCreatorFactory.java | 25 +-
.../bft/blockcreation/PqAnchorProducer.java | 347 ++ .../bft/blockcreation/PqAnchorProducer.java | 347 ++
.../common/bft/tools/PqRegistryHashTool.java | 231 + .../common/bft/tools/PqRegistryHashTool.java | 231 +
.../common/bft/FalconAttachIntervalTest.java | 232 ++ .../common/bft/FalconAttachIntervalTest.java | 232 +
.../common/bft/PqAnchorConfigTest.java | 691 +++ .../common/bft/PqAnchorConfigTest.java | 691 +++
.../bft/PqAnchorEmergencyConfigTest.java | 120 + .../bft/PqAnchorEmergencyConfigTest.java | 120 +
.../common/bft/PqAnchorIntervalTest.java | 186 + .../common/bft/PqAnchorIntervalTest.java | 186 +
@ -56,13 +68,14 @@ post-quantum.
.../bft/PqForkThresholdReachabilityTest.java | 355 ++ .../bft/PqForkThresholdReachabilityTest.java | 355 ++
.../bft/PqForkValidatorSetChangeTest.java | 427 ++ .../bft/PqForkValidatorSetChangeTest.java | 427 ++
.../common/bft/PqInertBinaryTest.java | 475 +++ .../common/bft/PqInertBinaryTest.java | 475 +++
.../bft/PqParentHeightAlignmentTest.java | 229 +
.../common/bft/PqRegistryBindingTest.java | 595 +++ .../common/bft/PqRegistryBindingTest.java | 595 +++
.../bft/PqRegistryHeightRefusalTest.java | 328 ++ .../bft/PqRegistryHeightRefusalTest.java | 328 ++
.../common/bft/PqRegistryRotationTest.java | 476 +++ .../common/bft/PqRegistryRotationTest.java | 476 +++
.../common/bft/PqSealPersistenceTest.java | 621 +++ .../common/bft/PqSealPersistenceTest.java | 621 +++
.../common/bft/PqSignedHeightTest.java | 403 ++ .../common/bft/PqSignedHeightTest.java | 403 ++
.../common/bft/PqStartupHistoryTest.java | 325 ++ .../common/bft/PqStartupHistoryTest.java | 325 ++
.../consensus/common/bft/PqV2Fixture.java | 233 ++ .../consensus/common/bft/PqV2Fixture.java | 233 +
.../qbft/core/messagewrappers/Commit.java | 18 + .../qbft/core/messagewrappers/Commit.java | 18 +
.../core/network/QbftMessageTransmitter.java | 25 +- .../core/network/QbftMessageTransmitter.java | 25 +-
.../qbft/core/payload/CommitPayload.java | 147 +- .../qbft/core/payload/CommitPayload.java | 147 +-
@ -91,7 +104,7 @@ post-quantum.
.../PqEmergencyShoutRuleTest.java | 78 + .../PqEmergencyShoutRuleTest.java | 78 +
.../PqForkGateFeedTest.java | 189 + .../PqForkGateFeedTest.java | 189 +
.../eth/sync/DownloadHeadersStep.java | 103 +- .../eth/sync/DownloadHeadersStep.java | 103 +-
75 files changed, 24137 insertions(+), 45 deletions(-) 76 files changed, 24406 insertions(+), 45 deletions(-)
create mode 100755 app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java create mode 100755 app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java
create mode 100755 app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java create mode 100755 app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
@ -125,6 +138,7 @@ post-quantum.
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkThresholdReachabilityTest.java create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkThresholdReachabilityTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkValidatorSetChangeTest.java create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkValidatorSetChangeTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqInertBinaryTest.java create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqInertBinaryTest.java
create mode 100644 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHeightRefusalTest.java create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHeightRefusalTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java
@ -1235,10 +1249,10 @@ index 000000000..578b6f5aa
+} +}
diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java
new file mode 100755 new file mode 100755
index 000000000..b042e1589 index 000000000..af694dcf9
--- /dev/null --- /dev/null
+++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java
@@ -0,0 +1,3709 @@ @@ -0,0 +1,3749 @@
+/* +/*
+ * Copyright contributors to Besu. + * Copyright contributors to Besu.
+ * + *
@ -4683,6 +4697,25 @@ index 000000000..b042e1589
+ final Optional<PqRegistryHash.ScheduleEntry> required = + final Optional<PqRegistryHash.ScheduleEntry> required =
+ PqRegistryHash.requiredHashAt(schedule, blockNumber); + PqRegistryHash.requiredHashAt(schedule, blockNumber);
+ if (required.isEmpty()) { + if (required.isEmpty()) {
+ // SCHEDULE BOUNDARY (2026-08-15). The only historical question ever asked one block
+ // below the schedule's first entry is about the certificate carried by the block at
+ // blockNumber+1, whose governing registry is the one bound EXACTLY at blockNumber+1.
+ // Answer it from that entry's VERIFIED bound registry, never from the unverified head
+ // file. Exactly one block: at blockNumber+2 below the schedule nothing changes.
+ final Optional<PqRegistryHash.ScheduleEntry> nextEntry =
+ PqRegistryHash.requiredHashAt(schedule, blockNumber + 1);
+ if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) {
+ final Optional<PqRegistryHash.Registry> boundNext =
+ PqRegistryHash.registryAt(schedule, set, blockNumber + 1);
+ if (boundNext.isPresent()) {
+ for (final PqRegistryHash.Entry e : boundNext.get().entries()) {
+ if (e.index() == validatorIndex) {
+ return new FalconPublicKeyParameters(FalconParameters.falcon_512, e.publicKey());
+ }
+ }
+ return null;
+ }
+ }
+ return headRegistryKeyOrRefuse( + return headRegistryKeyOrRefuse(
+ blockNumber, validatorIndex, "the schedule binds no registry at this height", historic); + blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
+ } + }
@ -4734,6 +4767,27 @@ index 000000000..b042e1589
+ blockNumber, validatorIndex, "no schedule was ever loaded", historic); + blockNumber, validatorIndex, "no schedule was ever loaded", historic);
+ } + }
+ if (PqRegistryHash.requiredHashAt(schedule, blockNumber).isEmpty()) { + if (PqRegistryHash.requiredHashAt(schedule, blockNumber).isEmpty()) {
+ // SCHEDULE BOUNDARY (2026-08-15). Same alignment as in keyAt: one block below the
+ // schedule's first entry the subject is the certificate of the block at blockNumber+1,
+ // governed by the registry bound EXACTLY there, so the answer comes from that entry's
+ // VERIFIED bound registry and mirrors what this method answers at blockNumber+1 itself.
+ final Optional<PqRegistryHash.ScheduleEntry> nextEntry =
+ PqRegistryHash.requiredHashAt(schedule, blockNumber + 1);
+ if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) {
+ final Optional<PqRegistryHash.Registry> boundNext =
+ PqRegistryHash.registryAt(schedule, set, blockNumber + 1);
+ if (boundNext.isPresent()) {
+ if (!boundNext.get().addressBound()) {
+ return null;
+ }
+ for (final PqRegistryHash.Entry e : boundNext.get().entries()) {
+ if (e.index() == validatorIndex && e.address() != null) {
+ return Address.wrap(Bytes.wrap(e.address()));
+ }
+ }
+ return null;
+ }
+ }
+ return headRegistryAddressOrRefuse( + return headRegistryAddressOrRefuse(
+ blockNumber, validatorIndex, "the schedule binds no registry at this height", historic); + blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
+ } + }
@ -17234,6 +17288,241 @@ index 000000000..f8a26fed3
+ } + }
+ } + }
+} +}
diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java
new file mode 100644
index 000000000..0067f4b49
--- /dev/null
+++ b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright contributors to Besu / AERE Network.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.besu.consensus.common.bft;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
+import org.hyperledger.besu.datatypes.Address;
+
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.OptionalInt;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.apache.tuweni.bytes.Bytes32;
+import org.bouncycastle.crypto.digests.KeccakDigest;
+import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
+import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * SCHEDULE BOUNDARY (2026-08-15): the PARENT-HEIGHT question at the first anchor.
+ *
+ * <p>WHAT WAS MEASURED, on a public node synced from genesis on the live chain. {@code
+ * PqAnchorSealsRule} judges the certificate carried by the block at the first anchor height H by
+ * asking the registry at the PARENT's height H-1, because that is the height the seals commit to.
+ * The schedule binds INCLUSIVELY from H, so at H-1 it binds nothing, and both height-resolved
+ * lookups fell back to the HEAD registry - which on a public node synced from genesis is EMPTY. A
+ * perfectly valid certificate was refused with "certificate carries index 0, which the registry
+ * does not bind to any validator address", and the node parked one block below the first anchor
+ * forever (measured: head 13,013,999 = H-1, 6,646 R2 rejections in the log).
+ *
+ * <p>THE REPAIR UNDER TEST. In the branch where {@code requiredHashAt(schedule, blockNumber)} is
+ * empty, before the head fallback: when the schedule has an entry at EXACTLY {@code blockNumber+1},
+ * answer from that entry's VERIFIED bound registry. The only historical question ever asked at H-1
+ * is about the certificate carried by the block at H, whose governing registry is the one bound at
+ * H; the alignment uses the verified v2 registry, not an unverified head file.
+ *
+ * <p>THE FIXTURE IS THE PUBLIC-NODE SHAPE, DELIBERATELY: no {@code aere.falcon.genesis}, so the
+ * head registry is EMPTY, and the registry reaches the node ONLY as history ({@code
+ * aere.falcon.registry.history}) bound by hash to the schedule entry at H. That emptiness is what
+ * makes the negative control possible - with a populated head, the H-1 assertions would pass
+ * through the old head fallback and prove nothing about the repair. {@link
+ * #baselineTheHeadIsEmptyOtherwiseNothingHereMeansAnything()} pins that shape.
+ */
+public class PqParentHeightAlignmentTest {
+
+ /** Anchor activation height H: the schedule's FIRST entry sits exactly here. */
+ private static final long H = 1_000L;
+
+ /** Height from which the staged threshold is non-zero, i.e. the fully armed regime. */
+ private static final long K_AT = H + 10L;
+
+ private static final int N = 7;
+
+ private static final long CHAIN_ID = 220_878L;
+
+ @TempDir private Path tmp;
+
+ private final List<FalconPrivateKeyParameters> privateKeys = new ArrayList<>();
+ private final List<Address> validators = new ArrayList<>();
+ private Path genesisPath;
+
+ /** A fixed 32-byte message, standing in for M(parent) of the first anchor block. */
+ private static final Bytes32 MESSAGE = Bytes32.fromHexString("0x" + "5a".repeat(32));
+
+ private Bytes sealByIndexZero;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ // The same v2 proof-bound manifest as PqRegistryHeightRefusalTest, bound at H.
+ final KeccakDigest kd = new KeccakDigest(256);
+ final StringBuilder manifest = new StringBuilder();
+ manifest
+ .append("{\"config\":{\"aereFalconRegistry\":{")
+ .append(PqV2Fixture.manifestHeader(N, CHAIN_ID, H));
+ for (int i = 0; i < N; i++) {
+ privateKeys.add(PqV2Fixture.privateKey(i));
+ validators.add(PqV2Fixture.address(i));
+ final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i);
+ kd.update(anchoredRow, 0, anchoredRow.length);
+ manifest.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, H));
+ }
+ manifest
+ .append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x")
+ .append("0".repeat(64))
+ .append("\":\"0x");
+ final byte[] anchoredHash = new byte[32];
+ kd.doFinal(anchoredHash, 0);
+ manifest.append(Bytes.wrap(anchoredHash).toUnprefixedHexString()).append("\"}}}}");
+
+ genesisPath = tmp.resolve("genesis-d228.json");
+ Files.writeString(genesisPath, manifest.toString());
+ // DELIBERATELY NOT setting aere.falcon.genesis: the head registry stays EMPTY, which is the
+ // public-node shape the defect was measured on.
+
+ // A genuine Falcon-512 signature by index 0 over MESSAGE.
+ final FalconSigner signer = new FalconSigner();
+ signer.init(true, privateKeys.get(0));
+ sealByIndexZero = Bytes.wrap(signer.generateSignature(MESSAGE.toArray()));
+
+ resetFalconSingleton();
+ // ARMED at H, exactly like the live fleet at its first anchor height.
+ PqAnchorProducer.useConfigForTesting(
+ new PqAnchorConfig(CHAIN_ID, H, Map.of(H, 0, K_AT, 3), OptionalInt.empty(), false));
+
+ // The registry reaches this node ONLY as history, exactly like the public package does it.
+ System.setProperty(
+ FalconSealSupport.PROPERTY_REGISTRY_HISTORY, genesisPath.toAbsolutePath().toString());
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+ final PqRegistryHash.Registry held = PqRegistryHash.loadAuto(genesisPath);
+ final PqRegistryHash.Schedule schedule =
+ scheduleFromGenesis(Map.of(H, PqRegistryHash.hashFor(held, CHAIN_ID)));
+ pqc.verifyRegistryBindingOrAbort(0L, CHAIN_ID, schedule);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ System.clearProperty("aere.falcon.genesis");
+ System.clearProperty("aere.pq.genesis");
+ System.clearProperty(FalconSealSupport.PROPERTY_REGISTRY_HISTORY);
+ resetFalconSingleton();
+ PqAnchorProducer.useConfigForTesting(null);
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 0. The fixture itself: the head must be EMPTY, or the two tests below pass through the old
+ // head fallback and the negative control cannot go red.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void baselineTheHeadIsEmptyOtherwiseNothingHereMeansAnything() {
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+ assertThat(pqc.addressForIndex(0))
+ .describedAs("the HEAD registry must be empty: this is the public-node shape")
+ .isNull();
+ assertThat(pqc.verify(0, MESSAGE, sealByIndexZero))
+ .describedAs("a head verify must fail for the same reason")
+ .isFalse();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 1. THE MEASUREMENT. At H-1, the question is about the certificate of the block at H, and the
+ // registry bound at H answers it.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void atTheParentOfTheFirstAnchorTheBoundRegistryAnswers() {
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+ assertThat(pqc.addressForIndexAtHistoric(H - 1L, 0))
+ .describedAs(
+ "SCHEDULE BOUNDARY: PqAnchorSealsRule asks at the PARENT height H-1 about the certificate carried "
+ + "by the block at H. The registry governing that certificate is the one bound at "
+ + "H, and it is VERIFIED; refusing here parks a syncing node at H-1 forever")
+ .isEqualTo(validators.get(0));
+ assertThat(pqc.verifyAtHistoric(H - 1L, 0, MESSAGE, sealByIndexZero))
+ .describedAs("and the key half must answer from the same bound registry")
+ .isTrue();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 2. THE BOUNDARY IS EXACTLY ONE BLOCK. At H-2 and below, nothing changes: the head fallback
+ // still answers, and on this empty head that answer is a refusal, exactly as before.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void theBoundaryIsExactlyOneBlock() {
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+ assertThat(pqc.addressForIndexAtHistoric(H - 2L, 0))
+ .describedAs("H-2 is not the parent of any anchor-governed height: old behaviour, refusal")
+ .isNull();
+ assertThat(pqc.verifyAtHistoric(H - 2L, 0, MESSAGE, sealByIndexZero)).isFalse();
+ assertThat(pqc.addressForIndexAtHistoric(0L, 0))
+ .describedAs("and deep below, the same")
+ .isNull();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // Helpers, borrowed from PqRegistryHeightRefusalTest.
+ // -------------------------------------------------------------------------------------------
+
+ private PqRegistryHash.Schedule scheduleFromGenesis(final Map<Long, String> entries)
+ throws Exception {
+ final List<Long> heights = new ArrayList<>(entries.keySet());
+ heights.sort(Long::compare);
+ final StringBuilder sb = new StringBuilder("{\"config\":{\"pqRegistryHash\":[");
+ for (int i = 0; i < heights.size(); i++) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ final long b = heights.get(i);
+ String h = entries.get(b);
+ if (!h.startsWith("0x")) {
+ h = "0x" + h;
+ }
+ sb.append("{\"block\":").append(b).append(",\"hash\":\"").append(h).append("\"}");
+ }
+ sb.append("]}}");
+ final Path p =
+ tmp.resolve("genesis-schedule-" + heights.size() + "-" + heights.get(0) + ".json");
+ Files.writeString(p, sb.toString());
+ return PqRegistryHash.loadScheduleFromGenesis(p);
+ }
+
+ private static void resetFalconSingleton() throws Exception {
+ final Field f = FalconSealSupport.class.getDeclaredField("instance");
+ f.setAccessible(true);
+ f.set(null, null);
+ }
+}
diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java
new file mode 100755 new file mode 100755
index 000000000..d902552cc index 000000000..d902552cc