diff --git a/IMPORT-PROOF-STARE-2026-08-15.md b/IMPORT-PROOF-STARE-2026-08-15.md index e75a563..7dd0cf4 100644 --- a/IMPORT-PROOF-STARE-2026-08-15.md +++ b/IMPORT-PROOF-STARE-2026-08-15.md @@ -38,3 +38,33 @@ carpeala in focul serii. **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 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. diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java index b042e15..af694dc 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java @@ -3442,6 +3442,25 @@ public final class FalconSealSupport { final Optional required = PqRegistryHash.requiredHashAt(schedule, blockNumber); 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 nextEntry = + PqRegistryHash.requiredHashAt(schedule, blockNumber + 1); + if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) { + final Optional 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( 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); } 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 nextEntry = + PqRegistryHash.requiredHashAt(schedule, blockNumber + 1); + if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) { + final Optional 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( blockNumber, validatorIndex, "the schedule binds no registry at this height", historic); } diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java new file mode 100644 index 0000000..0067f4b --- /dev/null +++ b/anchor/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. + * + *

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). + * + *

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. + * + *

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 privateKeys = new ArrayList<>(); + private final List

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 entries) + throws Exception { + final List 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/patches/0003-aere-pq-anchor.patch b/patches/0003-aere-pq-anchor.patch index cfee115..9db6516 100644 --- a/patches/0003-aere-pq-anchor.patch +++ b/patches/0003-aere-pq-anchor.patch @@ -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 -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 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 post-quantum certificate to the block hash; it does not make consensus 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 + .../cli/options/AerePqEmergencyOptions.java | 245 ++ @@ -25,14 +37,14 @@ post-quantum. .../common/bft/BftBlockInterface.java | 22 +- .../consensus/common/bft/BftExtraData.java | 50 +- .../besu/consensus/common/bft/FalconSeal.java | 88 + - .../common/bft/FalconSealSupport.java | 3709 +++++++++++++++++ + .../common/bft/FalconSealSupport.java | 3749 +++++++++++++++++ .../besu/consensus/common/bft/PqAnchor.java | 335 ++ .../consensus/common/bft/PqAnchorConfig.java | 1320 ++++++ .../common/bft/PqAnchorNotReadyException.java | 132 + .../common/bft/PqAnchorSyncModeGuard.java | 130 + .../common/bft/PqAnchorThresholdGuard.java | 252 ++ .../common/bft/PqRegistryBinding.java | 497 +++ - .../consensus/common/bft/PqRegistryHash.java | 2341 +++++++++++ + .../consensus/common/bft/PqRegistryHash.java | 2341 ++++++++++ .../common/bft/PqRegistryHashTool.java | 482 +++ .../consensus/common/bft/PqSealCache.java | 351 ++ .../consensus/common/bft/PqSealStore.java | 384 ++ @@ -40,7 +52,7 @@ post-quantum. .../blockcreation/BftBlockCreatorFactory.java | 25 +- .../bft/blockcreation/PqAnchorProducer.java | 347 ++ .../common/bft/tools/PqRegistryHashTool.java | 231 + - .../common/bft/FalconAttachIntervalTest.java | 232 ++ + .../common/bft/FalconAttachIntervalTest.java | 232 + .../common/bft/PqAnchorConfigTest.java | 691 +++ .../bft/PqAnchorEmergencyConfigTest.java | 120 + .../common/bft/PqAnchorIntervalTest.java | 186 + @@ -56,13 +68,14 @@ post-quantum. .../bft/PqForkThresholdReachabilityTest.java | 355 ++ .../bft/PqForkValidatorSetChangeTest.java | 427 ++ .../common/bft/PqInertBinaryTest.java | 475 +++ + .../bft/PqParentHeightAlignmentTest.java | 229 + .../common/bft/PqRegistryBindingTest.java | 595 +++ .../bft/PqRegistryHeightRefusalTest.java | 328 ++ .../common/bft/PqRegistryRotationTest.java | 476 +++ .../common/bft/PqSealPersistenceTest.java | 621 +++ .../common/bft/PqSignedHeightTest.java | 403 ++ .../common/bft/PqStartupHistoryTest.java | 325 ++ - .../consensus/common/bft/PqV2Fixture.java | 233 ++ + .../consensus/common/bft/PqV2Fixture.java | 233 + .../qbft/core/messagewrappers/Commit.java | 18 + .../core/network/QbftMessageTransmitter.java | 25 +- .../qbft/core/payload/CommitPayload.java | 147 +- @@ -91,7 +104,7 @@ post-quantum. .../PqEmergencyShoutRuleTest.java | 78 + .../PqForkGateFeedTest.java | 189 + .../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/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 @@ -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/PqForkValidatorSetChangeTest.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/PqRegistryHeightRefusalTest.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 new file mode 100755 -index 000000000..b042e1589 +index 000000000..af694dcf9 --- /dev/null +++ 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. + * @@ -4683,6 +4697,25 @@ index 000000000..b042e1589 + final Optional required = + PqRegistryHash.requiredHashAt(schedule, blockNumber); + 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 nextEntry = ++ PqRegistryHash.requiredHashAt(schedule, blockNumber + 1); ++ if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) { ++ final Optional 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( + 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); + } + 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 nextEntry = ++ PqRegistryHash.requiredHashAt(schedule, blockNumber + 1); ++ if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) { ++ final Optional 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( + 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. ++ * ++ *

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). ++ * ++ *

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. ++ * ++ *

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 privateKeys = new ArrayList<>(); ++ private final List

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 entries) ++ throws Exception { ++ final List 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 new file mode 100755 index 000000000..d902552cc