diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt new file mode 100644 index 0000000..4093c4c --- /dev/null +++ b/anchor/MANIFEST-sha256.txt @@ -0,0 +1,134 @@ +Per-file SHA-256 of the published anchor overlay. +Generated by aerenew/publish-bundle/stage-anchor.cjs from the authoritative overlay. +Aggregate hashes are not given on purpose: they depend on the locale of the machine that +computed them, and the same 276 files once produced two different aggregates here. + +329e4e2f93143cc8af092f0a951fa23619c12c4ec1793b053654f8e26e088da0 app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java +51764581814253cf76fa631e2d6557581a7bf9ae585a701009e37b80345f8867 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +129493150c0b13e8020bdf50c0c1ae07268d4f73a69a821b303bf4894451fa5e app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java +728c29c299edf6011f2c2798cbe930a043112e0e06b924e7edb54284ca44a758 config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java +a3b93f2602e9755d91358c9b3f473235f4d40ae23d912f797cdb03ed7e1348f9 consensus/common/build.gradle +657b2c652c7995976acab3feca7fec5e15acc66cdfe1d5cc23ced6813db17a5f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java +e32a03de9f1452bd7444a33b084516ae3399d7a5ae7430088714f5656d3a22ea consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java +3ab425b5b5f7d7c2199065a3905623d5c1c4c5922d892c6c4275415b0c69a2b4 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java +db1e80115ce59c8281fbf046cb37e153a49fe017c1675afd200e8125d3f6b538 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealScheme.java +167af0d87b017e1e15ecd2105426b009b6bdaeec5b52f7c1f710d212b0330765 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java +e8c8111a343cc993b950ac4e5b3992f16b8ee3d34c8827f00dd80d017884335b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducer.java +03043d2360c35de3b6f27d029b7ee83cb6c964f4b827a504799607b8bfe1cdcc consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupport.java +65e33e639c58adb30045e88c770460757305acab31ac884e4c6b36f1c410ef12 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistry.java +e9b30713dea69601dd29b3e460d81585a21f5c48286d18f4d3bc5d4c4ec83c19 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java +852113d420f2c6b82945423a18591c3b2bd1018febe1a52e8b4e42a996c85114 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java +c6285ca43331781e1d350a07d58256d02b8b3a74b96b6469c0dca33ecfb6c8cd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java +f27952a25bad02d2939a356f1bac8800ff8fb08023c6bdfb1c6d0e51b5a206da consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java +81ddbf2b40f79bf0e382c507235f07ad74937ffd455e387c265880bdcf4e2b5c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSyncModeGuard.java +e16b2c654fe21893c7972dac71f3ffafd1e4e8efbecdb6f95051820e36fbd48e consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuard.java +91755c5a013b287d83d5e4d2d32ca820bced6d1d8560f0a3c76418797f0e69fe consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2.java +78ecea21ff746564984dff40e1067808789f9bc5075ca3b5e8af92e0ea137c72 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBinding.java +f67a205036550f7d0f1bfed2abe7b89abd73e80614adff8d696b33c580269a6f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHash.java +3fbd850e00e487c40c61310d894d6e1f765e9098e5c55438f9b06ab8fb92d45b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java +e85f4be696700184f9700cc531b4f3df313e9fb8cff8e2bc39d24241a769addb consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSchemeSchedule.java +d2e8b34f29d0dd83a66b78b53ce430404834cdf9ba334abeb72481d5d681aeb9 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java +e8f7a8c4d8f1626d8efd167e5096de14ca17d40af58577737c5ef6a769dcc4d5 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java +a235b3ec8c69665c525d7c74a084d589904879f5cd33cb87e1d1f38eabba8a15 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java +addba40c0d931a3ecfa3b2f0179311dacaa604884e3ebb1d79f958bd913f72e2 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SchemeSeal.java +285f6a7c1188a387fb1ac21f5ab809595ef48d936b40aced94047525e6def0a6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java +352ffd303d2fcdc0d09ab943cebb2cd22b9c6329e129eb522f51fed8357f877d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealSchemes.java +1ef188292858db2a6b70c074a2477223bf96e2762a3711c8a993404eb2db2cfa consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SlhDsaSealScheme.java +b6c9ecbf3cd2ee73111984cd04a89c32ee56a1cc699a197b65ef7c10001b8cbc consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java +5c8861eba1ea697d8deb88139d92c6ea6636b29d76c881b13e7f6c841ba2324f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java +6ace00e18914a1558563e689b7427654b5e6226a31447bef1d0416d993f78840 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java +fab7a67ca190e6cb2469bac842112116208ea5e84e47b064d6617c36be14ae23 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ThresholdReachabilityTest.java +5534cfab3bd59968823265655351491abef8ea73f594677a62bf47c8e88deb54 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ValidatorSetChangeTest.java +da249c59f356e06928f73543d5529ae1911e4613a8125477f475b1dd54daed62 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D079ForkArmingTest.java +9a794d7a4010ff5c561008229cb2ae97d5f79367641190c97cde8802b83daa72 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D081RegistryRotationTest.java +a4c979fcd296f974c58b372d763b2bbf7fa96997f0fd52d66e5ac14185d5b8ab consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D140FleetRestartArmingTest.java +2e81e02a419cdb2053b67e2bc9e699af1366955fa18d351178637b64a48a27be consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D141SealPersistenceTest.java +42dd6396583aa19475e23c9781f568958d9789f0dba084dc63ac9833203f3f1f consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D146ArmingGateTest.java +75fca200a2ac9e7226c70d96916a4ae33201a8acb466d0fdc591576e09edb3fc consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D147InertBinaryTest.java +acd5c1e7f49dbbf444c19c91386bbd03b9298d952d2d13721793670103e71ee9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D177NeutralNamesTest.java +bc6b58570f835276324a5bd2704e7de8cac4e0693bd0138199d149c1ec7d4174 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D2CallerIntentTest.java +4aeab501c83f0d7fbe7a45c99e2035fd034b055b2a6cd5925026e29e4e427849 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D2RegistryHeightRefusalTest.java +c574ec22e642ac464b1728d27e4bb26202448a45c044f08935cc0a0efa0297d9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FalconAttachIntervalTest.java +0eb20506f851c1510bcaf659db8c5b0d384cdc60ed06192a03a665f0ee2b3912 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducerTest.java +186c4c92199400783ee3424231818dd440b0da41aeb3d8bd643cd13759998250 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupportTest.java +834861639fd119c1653e6a3977ffbe4c2d4dce63a5741f1fe5a087643b86ed74 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java +220fceda0f5292054e322bb7df2d5258905a8a7b6f9febd802789d3946a0c01c consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java +6e2ae09f62765d6e558fdf56a124c890681c2b2c3d410bf8aa497820cd76c3a5 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java +7470d72271dd4f9de96094b3d1c6dc4b5b5ea62e46a058d4e154e3742d3ccef1 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorIntervalTest.java +e2487ff508ffb51bb61ca19531f4c4e68c5981611fd9fec5c92a2965f0bb8f47 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java +dabb059478da5259c309e6637e987ab35ef7b69cc286e41f936e4f3e83d3f082 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java +c20743ce1b54af2b7b9d42366bef14678fd94aa8d1dc98eabf21ff0f9ae15302 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCacheHygieneTest.java +0bf1c8a9cdd91d7c34053d6e5b4ff929787f34eb7aa7d6eed26fc5c94fa1c42b consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCostTest.java +23daeb4888c8de8337518a27ef60e42573ebafaf340075e8ce9236f0d8d580f6 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java +09c41c77408acc4711e4c46175485bf41babff0d0a8ec7481670fddfd28967b0 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java +6ff9586cf0d544590585ad27227f7775f1dd86c61ccdd475321180d4e53f47d2 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuardTest.java +72e58681267664a864dba5a371ffbc6f296b524c2dee248893b17c12da113f00 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2Test.java +49c95a24ce4890fe79b9167c196ba8da460e03bdddfee5652944e130fed8ee78 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqArmingGateTest.java +74e2629acdcf9242c32340679914fa40f50d0a9479c83018b9b7a0733397a843 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqCallerIntentTest.java +067a3027f21681d82b4bf7ac34ff1590f37213152ead9d246c6d45162e5a2d0b consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqFleetRestartArmingTest.java +cb674b2461f2527cea044722273972d0e8cdac61d6f8b6a7fab3b875abf05a6b consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkArmingTest.java +64db7075e47fb32b4756a43cf218bfcec46e915eaadeb10f5f04716c6f615d66 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkThresholdReachabilityTest.java +7da106ab48ef4145fa73de5ce817210b5320ccdf06433a688e1a2f9ee69ee53a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkValidatorSetChangeTest.java +b28edbcd65bb9228d1a36589415eb803871cec6e9f2d7a247bc45a082f3b7721 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqInertBinaryTest.java +f260d284f936307ac4c31142cbea0359ded1197880b8a960ac27dd7beb5c3b53 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java +19ff0e11861d44b582fe39d09aaeb63ee0a5b9ba07d036da85a70ea612dab6dc consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java +97281811ae1fe6fae8001d0d64e11d4779be153547ab24c779a54aba3e211c88 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHeightRefusalTest.java +7ec10b4fa5ac09e4d980f5d180c75eecc892d95c2f81d97c6140e64ada34654d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java +36cd369e8386cac85501f7d8a56f9604ec39e8bd89a8d754cabe85e4fb6b8e9a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java +44b0010772765eacb8fb720cc0570b0a8f681a3a329060271e67b7c518ddb25b consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java +e0bc7a465dbb2dd7efa5f1830211a1a45c6554c300b2088dd70bb9677b0ac217 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java +9a371ec367cda846640c6da18c8bf7d921a6882cfc024dc0c86d4f965cae0c63 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java +e65ba92e288aebe768909b1fee5c0b8850be74b3658c6242e6abdb91409a9521 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqV2Fixture.java +56426ba6bb10df29f5ef5f5959b1646dda5a6d9e7db9904ceb7ad1070ad750b8 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PreparePqAttachGateTest.java +d48d62c6ea0784864a5987a804a28855265b2180c409ba3c5b4434d21b805e1d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java +22ade1aa2d8254d1f7ff02bbbcb4d02155546a52fca3421b61eea1129b73ee48 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Commit.java +046c67d18ba623cd01390b6ffef945b0dfa245fbf0cc79889b7670ab465b7992 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java +03173f2029f767e10cc978b5576b9557efa783dab2ef105aa856f6d5e08b9352 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java +37bda0f126c1333cf854a33c64cc98cedcdc8c0c2ec4cc3ba1f6736e7302ddff consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java +861a2e6f81df1c33755f28ca50bf88b6b0b197f7a7a1d646c165788d192160ca consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayload.java +64c3ab6f9b67eb2ed126bca89ffc0af49bebc17bc8a41b56efc19e51188ca75d consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java +252f9a86ca17cc6362780af264dbc4da4d842191467427a809ff1e4baa7640af consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java +07834160f12c4f28c567959c358176708b17742cc7cc4f49542b165efef5f1c8 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java +a22d857867c5b7c9ccdf9185c661870eafe6b481573189202b2cc34d9fbf3e2d consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java +601b0cccf32f8ebe327c7581e0e9b3f9f67de49d9fb646326d6b49ef2138c641 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java +7f9fb13a7bbe3015bbef6329f750f2b35fb3744895cd3e6599af90450fab6a73 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java +ec53f8e401a19345a067223f482f27a95cb9c3153d5bee0bfea9fd622bc0db01 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java +5ab52433f770537e838b15e23abfc7b24eaa733ea26577451d5e32226d617879 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcement.java +60eb9c5a8e3f464aa42915909e402bf59af9bad4e28631538dcf2f5ca52a9569 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PrepareValidator.java +36dd77cdea980e4de0090e86ae2ca75d1af62a9477fe056c7fdde06422617898 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayloadHybridTest.java +fe25255de4b5fa6d86412fa4c48218180f549f0a4ffea5b5cc0a7d19a5be4cd9 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayloadPqTest.java +0bb2c1e31a7f29e9b07fb7030567746fc6d79460ed4739d5ecafac44501f7bb5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqLateSealSalvageTest.java +8c40173c7b248edec7c29ee9e049f9e269f4725b637bceb8734735fc73eb9ca2 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidatorPqEnforcementTest.java +401c25f63abb248dfe60b69676a34ea255bb669b4bc58e45916f6eecea3cb870 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcementTest.java +c0853ed97c53d54951e25ad6d0b70c0299dedd859cc7c44da64e3751d0e0de33 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitPlumbingTest.java +5e0bb0ecc77ffb06f232e1aa81cca6870e5455af7c2846ae54d5f477f1bbb88b consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqHybridEnforcementTest.java +5a47d247d7bcda77b57f5c906c3cee1af826785416012fafe9cfa49d63671388 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareAgilityTest.java +1939e33cc8ea81782e5e17d68e2b59c001e4207ed8ef07acd1d2a166a047dcb3 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcementTest.java +dc9f9e862a11f0135d26976176a1d1adac3e84fd5f1e06572d8727262cb3374f consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PrepareValidatorPqWiringTest.java +83dd971f66ed63103d09db5283240556bf1db1e1925c2a07808c7f59d47433ad consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangeJustificationPqTest.java +79dea5e85b968af696bf57d51d0de175a4c025537200402e3594a4a8290d1a29 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java +7d0a75818ac64b601b79c32b8289eedda8e2300dbb0f5e9186084f4e01b04d26 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java +47fbdd8639c464bf19bac3a0ba540af4d99f79e9df02c2ad586a19a2aebd037d consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java +cfb0aad408ab620f28b09914ece7d8d3d09f9e6bbb503070e28d9ef39c023dee consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/blockcreation/QbftBlockCreatorFactory.java +cbec6f4e280250ddefe5c79c453b59a07be6435e6f88d5614e92168c855e4693 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRule.java +e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRule.java +ab0fcd8722dcb76560f0ef8fda8af9c2b8b6ec9b468326aaf616c853526f7f81 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestAttachedRule.java +81a46071e77eb72aac107e6afca9e50612a81b66c5f17999815652eeda005248 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java +05ee2e97644c9a79ee082a5030b6e2a6745933d26cb8238a6339e6006c47d7e3 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java +2add2a7733668e11b50ddd64b05bb37db26a18284a636bbe720c2cb1f6aa28ed consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRule.java +8ac99e94c89f61f0281199cf369e282fc4f9fb1a3414a1650766e1d7548e76f5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java +52b56f157500ae3527b7e55c51786915bb5d6980065668f1e7b8e297bf7125ef consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftAnchorRuleWiringTest.java +5a18c7fee308654d9557fc507ca7a64704bdac5d13835e7df47a3dfb41519902 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRuleTest.java +8f27193a286d1e6bb4c84f98e5af9821ee9a22873a26288fad67256c710a51cd consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D078GateFeedTest.java +ebc5811c3a765b1175023d2c767eb8c71f4f2bdbb63ec0b354294a3f0ac15dfa consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D079ArmedWithoutRegistryTest.java +ad018cba0a3fe7f018b11c6c6a2d45e3f5547342ec45b620a4df2595801ae71a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealLogThrottleTest.java +3895d10bcf5ffbdaf0506503a0d9e3d72c600288def268fd5e9f68c4a042e162 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRuleRetirementTest.java +e2df575ee4d6ab5bd961b0886ece3d3c392a50193f1d3256a438a72da9e20d10 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRuleTest.java +77d16fd35ca5607e9702f39cfd9a24a7e8523f631a73e82138763e6d5b718e7b consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java +a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java +535cca4f1083c8a25bd29877e988b99704666853c1fe8b0ba32d71efe4e15f37 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java +321cb7e6923f77e078b523316650ac49136cf8d4df506b9bb235a32795c69611 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqArmedWithoutRegistryTest.java +484bc74ff3d1e52d25631264d83af70c2b695e973c7360b0f3db471ab5c6613a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRuleTest.java +49c3fd97e7985966531928af173b6f38ac48ff9dc8b5be1106e5b7bfb9c7b2b6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqForkGateFeedTest.java +3b690b72e80a0f9eaf868a3fa3f99ace42dad74dc30b2c98b9f3ca5a023917c0 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadHeadersStep.java diff --git a/anchor/README.md b/anchor/README.md index f8323e6..2783810 100644 --- a/anchor/README.md +++ b/anchor/README.md @@ -52,7 +52,8 @@ The threat is not ours to claim. It is the long-range attack, and the peer-revie 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 old validator keys becoming compromised, by any means. The quantum framing is +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 @@ -122,6 +123,45 @@ now pinned: change one byte of it and the digest no longer matches, and the head ## 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 — +`stage-anchor.cjs` in our 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 | **no** — no node sets the property | +| Post-quantum seal on PREPARE, enforcement | yes | **no** — no node sets the property | +| Hybrid Falcon + SLH-DSA certificate | yes | **no** — needs new keys, not generated | + +Everything in the "no" rows is **disarmed by absence, not 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 is not armed anywhere and why its +activation height is a decision that has not been taken. + +### 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 @@ -173,6 +213,14 @@ and the validation rules. 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 PREPARE layer has not run on a live network.** It has been exercised on a test network, + including a mixed run against a second, independent client implementation, and it has not been + armed on chain 2800 or anywhere else that carries value. Test-network evidence is evidence about a + test network. +- **The hybrid Falcon + SLH-DSA certificate has never been signed with a real key.** The scheme + layer is here and a second algorithm passes through the same consensus code untouched, which is + what the tests measure. Generating hybrid validator keys is a separate decision that has not been + taken, so no hybrid certificate exists on any chain. ## One claim we retracted, on purpose diff --git a/anchor/app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java b/anchor/app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java index 0d68534..509c81f 100644 --- a/anchor/app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java +++ b/anchor/app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java @@ -1,5 +1,5 @@ /* - * Copyright contributors to Besu / Aere Network. + * 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 @@ -32,10 +32,9 @@ import picocli.CommandLine; * If the anchor misbehaves on the live chain at three in the morning, the person on the other end of * the page has to be able to stand it down with a RESTART. Until this class existed the controls * were real but reachable only as system properties and environment variables, which in practice - * means editing a service unit or a wrapper script on every node of the fleet under time pressure, - * in a file whose syntax nobody remembers, with no {@code --help} to check against. Two of these - * options were already named in the javadoc of {@code PqAnchorConfig} as though they existed. They - * did not. + * means editing a systemd unit or a wrapper script on seven machines under time pressure, in a file + * whose syntax nobody remembers, with no {@code --help} to check against. Two of these options were + * already named in the javadoc of {@code PqAnchorConfig} as though they existed. They did not. * *
The three controls, and why exactly these three. * @@ -49,10 +48,10 @@ import picocli.CommandLine; * one to reach for when the chain has stalled because proposers cannot gather K seals - too * many validators down, a key rotation half-done - and the rest of the scheme is fine. *
Every one of them shouts. A quiet way out is worse than no way out, because it will be @@ -65,10 +64,10 @@ import picocli.CommandLine; *
How they take effect, and why through the properties. Each option writes the SAME system * property the control has always read, before anything reads it. That is deliberate: it leaves * exactly one place where each decision is made, so the command line cannot mean something subtly - * different from the environment variable, and the code that was measured under the registry - * binding work and under the anchor work is the code still doing the deciding. Precedence is - * command line, then system property, then environment variable; which source won is written into - * the log so an operator never has to guess whether the flag took. + * different from the environment variable, and the code that was measured under A8 and under the + * anchor work is the code still doing the deciding. Precedence is command line, then system + * property, then environment variable; which source won is written into the log so an operator never + * has to guess whether the flag took. * *
Deliberately LOCAL, not on-chain. A halted chain cannot deliver a height-scheduled * configuration change. The only control that works when the chain is ALREADY STOPPED is one that diff --git a/anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java b/anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java index c245c06..117f2f7 100644 --- a/anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +++ b/anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java @@ -264,7 +264,8 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder { // eligible Falcon seal(s) ... threshold is 3", over "Attachment stays OFF (fail-safe)". // With K=0 the chain heals itself. With K>0 it NEVER heals. // - // A simultaneous restart of the seven is not an exotic scenario: it is a power cut, a scheduled + // A simultaneous restart of the whole fleet is not an exotic scenario: it is a power cut, a + // scheduled // kernel update, or any procedure that starts the fleet all at once. // // The repair invents nothing and weakens no check: it does here, once, exactly what the import @@ -310,7 +311,7 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder { } } - // AERE REGISTRY-BINDING (2026-08-01): bind the Falcon registry to consensus. + // AERE A8 (2026-08-01): bind the Falcon registry to consensus. // // Deliberately placed immediately after the attachment guard and before BftExecutors, for the // same reason: a chain head exists here, and the network and the QBFT state machine have not @@ -321,13 +322,13 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder { // The two guards answer different questions - "is the activation height sane relative to this // chain" and "is this the registry this chain requires" - and both have to be true. // - // AERE REGISTRY-BINDING (2026-08-02): the SCHEDULE comes from the genesis configuration BESU - // BOOTED WITH, not from a genesis file re-opened by path from a system property. Re-reading a - // file would have reproduced the defect one level up: the enforced binding would again depend - // on a local file a node can be pointed at wrongly, and a node reading a stale copy would - // enforce a stale schedule, or none, in silence. Read from GenesisConfigOptions there is no - // second file: the value enforced comes out of the same object that produced this node's - // genesis hash, so a node that disagrees about the schedule already disagrees about the chain. + // AERE A8 (2026-08-02): the SCHEDULE comes from the genesis configuration BESU BOOTED WITH, not + // from a genesis file re-opened by path from a system property. Re-reading a file would have + // reproduced the defect one level up: the enforced binding would again depend on a local file a + // node can be pointed at wrongly, and a node reading a stale copy would enforce a stale + // schedule, or none, in silence. Read from GenesisConfigOptions there is no second file: the + // value enforced comes out of the same object that produced this node's genesis hash, so a node + // that disagrees about the schedule already disagrees about the chain. // // MEASURED, and it is the input that decides the hash: the live chain 2800 genesis carries // config.chainId = 2800, so getChainId() is PRESENT and the 0L fallback below is not the value @@ -378,13 +379,12 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder { // with zero seals, nobody reached K, nobody could propose, and so nobody sent another Commit. // The same circular deadlock, one level down. // - // WHY THIS IS NOT A REGISTRY-TRUSTED-FROM-A-FILE IN NEW CLOTHES, and this is the whole security - // argument: the seal is SELF-VERIFYING. Every seal read from the file is cryptographically - // verified again against the anchored registry, over an M rebuilt from the head header this very - // process has just loaded, exactly as the producer does at selection time. A forged file cannot - // inject a seal without forging a Falcon-512 signature; all it can obtain is the empty cache an - // absent file already gives. The defect back then was a REGISTRY of keys trusted because it sat - // in a file. + // WHY THIS IS NOT A8 IN NEW CLOTHES, and this is the whole security argument: the seal is + // SELF-VERIFYING. Every seal read from the file is cryptographically verified again against the + // anchored registry, over an M rebuilt from the head header this very process has just loaded, + // exactly as the producer does at selection time. A forged file cannot inject a seal without + // forging a Falcon-512 signature; all it can obtain is the empty cache an absent file already + // gives. A8 was a REGISTRY of keys trusted because it sat in a file. // // Deliberately here: the registry is already armed by the block above (otherwise no seal could // resolve and the restore would have gone quiet for nothing), the chain head exists, and the @@ -684,8 +684,8 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder { } /** - * AERE REGISTRY-BINDING: read a genesis {@code config.*} value that Besu itself does not model, - * out of the genesis configuration THIS NODE BOOTED WITH. + * AERE A8: read a genesis {@code config.*} value that Besu itself does not model, out of the + * genesis configuration THIS NODE BOOTED WITH. * *
Besu's {@code GenesisConfigOptions.asMap()} cannot be used for this: it is an allow-list of * the keys Besu knows about, so a key of ours is simply absent from it and the guard would read @@ -705,10 +705,9 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder { private com.fasterxml.jackson.databind.JsonNode aereGenesisConfigNode(final String key) { if (!(genesisConfigOptions instanceof JsonGenesisConfigOptions)) { LOG.warn( - "AERE PQC REGISTRY-BINDING: the genesis configuration is a {}, not the JSON-backed " - + "implementation, so config.{} cannot be read and the Falcon registry binding is " - + "NOT ENFORCED on this node. A binding everybody believes is on and is not is worse " - + "than no binding.", + "AERE PQC A8: the genesis configuration is a {}, not the JSON-backed implementation, so " + + "config.{} cannot be read and the Falcon registry binding is NOT ENFORCED on this " + + "node. A binding everybody believes is on and is not is worse than no binding.", genesisConfigOptions.getClass().getName(), key); return null; diff --git a/anchor/app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java b/anchor/app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java index e74459f..b77a549 100644 --- a/anchor/app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java +++ b/anchor/app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java @@ -1,5 +1,5 @@ /* - * Copyright contributors to Besu / Aere Network. + * 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 diff --git a/anchor/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java b/anchor/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java index e0d6a60..7958160 100644 --- a/anchor/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java +++ b/anchor/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java @@ -647,8 +647,7 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions { } /** - * AERE REGISTRY-BINDING: the raw genesis {@code config.*} value for a key Besu does not model, or - * null. + * AERE A8: the raw genesis {@code config.*} value for a key Besu does not model, or null. * *
WHY THIS EXISTS. {@link #asMap()} is an allow-list of the keys Besu knows, so a key of ours
* is absent from it, and a guard reading it would conclude "no schedule" on a genesis that
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
index 578b6f5..9f290a1 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealScheme.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealScheme.java
new file mode 100644
index 0000000..0f0090c
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealScheme.java
@@ -0,0 +1,104 @@
+/* AERE crypto-agility: Falcon-512 behind the SealScheme seam. The registry form is the raw
+ * Falcon h vector, 896 bytes, exactly what the signer registry stores today (measured on the
+ * proof-network registry files, registru-PROBA-v2-*.properties: 896 per entry). The 897-byte
+ * form pk(897) = 0x09 || h belongs to the 0x0AE1 PRECOMPILE input format, one header byte above
+ * this layer; confusing the two costs a red test, which is exactly how this comment was earned. */
+package org.hyperledger.besu.consensus.common.bft;
+
+import java.security.SecureRandom;
+import java.util.Optional;
+
+import org.bouncycastle.pqc.crypto.falcon.FalconKeyGenerationParameters;
+import org.bouncycastle.pqc.crypto.falcon.FalconKeyPairGenerator;
+import org.bouncycastle.pqc.crypto.falcon.FalconParameters;
+import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
+import org.bouncycastle.pqc.crypto.falcon.FalconPublicKeyParameters;
+import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+
+/** Falcon-512 as a pluggable seal scheme. */
+public final class FalconSealScheme implements SealScheme {
+
+ /** Registry form: the raw public h vector for Falcon-512 (no precompile header byte). */
+ public static final int PUBLIC_KEY_LENGTH = 896;
+
+ private record Pub(FalconPublicKeyParameters params) implements PublicHandle {}
+
+ private record Priv(FalconPrivateKeyParameters params) implements PrivateHandle {}
+
+ @Override
+ public String id() {
+ return "falcon-512";
+ }
+
+ @Override
+ public byte wireId() {
+ return 0x01;
+ }
+
+ @Override
+ public int publicKeyLength() {
+ return PUBLIC_KEY_LENGTH;
+ }
+
+ @Override
+ public Optional NAMING, and this is not pedantry. This line used to read "for the hybrid post-quantum
* consensus seal". AERE DOES NOT HAVE POST-QUANTUM CONSENSUS and must never be described as having
* it: proposer selection and finality are classical secp256k1 ECDSA QBFT, and the post-quantum
- * layer is signature, precompile and account level. Our audit scope tells reviewers to FLAG that
- * phrase wherever it appears in code comments. It appeared here.
+ * layer is signature, precompile and account level. Our own audit scope dossier
+ * ({@code audit-package-pq-consensus/scope/AUDIT-SCOPE-DOSSIER-2026-07-12.md}, section 3) tells
+ * reviewers to FLAG that phrase wherever it appears in code comments. It appeared here.
*
* This deliberately does NOT ride the shared secp256k1 {@code NodeKey} / {@code SecurityModule}
* singleton (that interface returns ECDSA R,S and is used chain-wide for transactions and devp2p).
@@ -111,7 +112,7 @@ import org.slf4j.LoggerFactory;
* whenever {@link #addressForIndex} returns null or {@link #verify} returns false. A node whose
* registry file fails to load therefore rejects every header carrying a certificate, i.e. it halts
* itself, and two nodes with DIFFERENT registry files disagree about which headers are valid. That
- * is the unbound-registry defect, and it is why the registry has to be bound to genesis by {@code pqRegistryHash}
+ * is defect A8 and it is why the registry has to be bound to genesis by {@code pqRegistryHash}
* before any of this is armed. While both of those gates are unset - which is the state of chain
* 2800 today - the subsystem is log-only and the old sentence holds; it is not a property of this
* file, it is a property of the configuration.
@@ -135,6 +136,10 @@ public final class FalconSealSupport {
private static final String ANCHOR_SLOT =
"0000000000000000000000000000000000000000000000000000000000000000";
+ /** Cate sigilii a emis acest nod pe PREPARE. Vezi {@link #preparesSealed()}. */
+ private final java.util.concurrent.atomic.AtomicLong preparesSealed =
+ new java.util.concurrent.atomic.AtomicLong();
+
private final boolean signingEnabled;
private final int localIndex;
private final FalconPrivateKeyParameters localPrivateKey;
@@ -150,7 +155,7 @@ public final class FalconSealSupport {
private final boolean genesisAnchored;
/**
- * AERE GENESIS BINDING: the registry file this node ACTUALLY used, and which of the three sources it came from.
+ * AERE A8: the registry file this node ACTUALLY used, and which of the three sources it came from.
* Recorded so the consensus-binding guard can re-read exactly that file and hash it, rather than
* hashing something adjacent to it. Null when no registry is configured at all.
*/
@@ -158,11 +163,11 @@ public final class FalconSealSupport {
private final PqRegistryHash.SourceKind registrySourceKind;
- /** Outcome of the genesis-binding guard; NOT_CHECKED equivalent is null until it has run. */
+ /** Outcome of the A8 registry-binding guard; NOT_CHECKED equivalent is null until it has run. */
private volatile PqRegistryHash.GateState registryBindingState;
/**
- * AERE GENESIS BINDING (per-block half): the schedule the startup guard actually enforced, the registry object
+ * AERE A8 (per-block half): the schedule the startup guard actually enforced, the registry object
* it hashed, and the chain id it hashed under. Held so that {@link #registryBindingSatisfiedAt}
* can answer the SAME question on the per-block consensus path without re-reading a file 61
* million times a year, and without a second, drifting copy of the decision.
@@ -176,17 +181,17 @@ public final class FalconSealSupport {
private volatile PqRegistryHash.Registry registryBindingLoaded;
/**
- * HEIGHT SCHEDULE: every registry this node holds, bound to the schedule entry each one satisfies.
+ * D-081: every registry this node holds, bound to the schedule entry each one satisfies.
*
* {@link #registryBindingLoaded} above is the registry for the HEAD, and it is what the node
* signs with. It is kept because every diagnostic message names it. This field is the whole
* scheduled history, and it is what the per-block binding question and height-resolved
* verification are answered from. With no history configured the set holds exactly the one
- * registry above, so both answers are bit-for-bit what they were before the height schedule existed.
+ * registry above, so both answers are bit-for-bit what they were before D-081.
*/
private volatile PqRegistryHash.RegistrySet registryBindingSet;
- /** HEIGHT SCHEDULE: per (schedule entry, index) Falcon public keys, built on demand from the set. */
+ /** D-081: per (schedule entry, index) Falcon public keys, built on demand from the set. */
private final Map WHY THIS EXISTS AT ALL. The genesis-binding guard is a CONFIGURATION guard: it refuses to start a node
+ * WHY THIS EXISTS AT ALL. The A8 guard is a CONFIGURATION guard: it refuses to start a node
* whose registry file is not the one the chain names. That refusal is correct and it is
* fail-closed, and it also means a single bad registry file pushed to the fleet takes the fleet
* down and the only documented way back was to rebuild or to hand-edit a service unit at whatever
@@ -289,8 +294,8 @@ public final class FalconSealSupport {
* chain head at startup. Sized from the measured startup-to-first-commit window (45.2 s, i.e. ~46
* blocks on the 1 s scratch fleet) with a wide safety factor, so that a node which is still
* opening its database, syncing and joining the network cannot have the activation height arrive
- * underneath it. Chains with a sub-second block period must RAISE this in proportion: the
- * margin has to cover the same wall-clock startup window at the faster block rate.
+ * underneath it. Chains with a sub-second block period must RAISE this: see
+ * GARDA-ACTIVARE-2026-08-01.md for the sizing formula.
*/
private static final long DEFAULT_MIN_ATTACH_FUTURE_MARGIN = 1024L;
@@ -311,7 +316,7 @@ public final class FalconSealSupport {
private static final String ADMISSION_RECEIPT_FILE = "aere-falcon-activation.receipt";
/**
- * HEIGHT SCHEDULE: the registries this node holds for SCHEDULED HEIGHTS IT IS NO LONGER AT, as a
+ * D-081: the registries this node holds for SCHEDULED HEIGHTS IT IS NO LONGER AT, as a
* comma-separated list of files. Env: {@code AERE_FALCON_REGISTRY_HISTORY}.
*
* WHY A SECOND PROPERTY AND NOT A LIST IN THE FIRST. {@code aere.falcon.registry} names the
@@ -338,7 +343,7 @@ public final class FalconSealSupport {
public static final String ENV_REGISTRY_MISMATCH_ALLOW = "AERE_PQ_REGISTRY_MISMATCH_ALLOW";
/**
- * AERE LATE-ANCHOR HEIGHT: the DECLARED height at or after which the on-chain late-anchor registry contract is
+ * AERE D-079: the DECLARED height at or after which the on-chain late-anchor registry contract is
* expected to be observable. Mandatory whenever a blocking fork height is armed over a late-anchor
* registry that is still pending; see {@link #validateAnchorObservationHeightOrAbort}.
*/
@@ -351,7 +356,7 @@ public final class FalconSealSupport {
private final long attachBlock;
/**
- * AERE LATE-ANCHOR HEIGHT: the BLOCKING height, resolved and validated exactly ONCE, at construction, and
+ * AERE D-079: the BLOCKING height, resolved and validated exactly ONCE, at construction, and
* owned thereafter. Long.MAX_VALUE means never blocking.
*
* It used to be a property read on every call to {@link #forkBlock()}, with a
@@ -364,7 +369,7 @@ public final class FalconSealSupport {
private final long forkBlock;
/**
- * AERE LATE-ANCHOR HEIGHT: the height at and after which the on-chain LATE-ANCHOR registry contract is expected
+ * AERE D-079: the height at and after which the on-chain LATE-ANCHOR registry contract is expected
* to be observable, as DECLARED by the operator. Long.MAX_VALUE when undeclared.
*
* This value does not activate anything. It exists because without it the ordering that the
@@ -376,7 +381,7 @@ public final class FalconSealSupport {
private final long anchorObserveBlock;
/**
- * AERE LATE-ANCHOR HEIGHT: first height at which this node validated a header at or after the blocking height
+ * AERE D-079: first height at which this node validated a header at or after the blocking height
* while the anchored registry was NOT active, or -1 if that has never happened.
*
* This is the residual the configuration guard cannot close: an operator may declare the
@@ -407,13 +412,13 @@ public final class FalconSealSupport {
private final AtomicBoolean loggedCoverageBlocked = new AtomicBoolean(false);
/**
- * LOOKUP HARDENING (b): the once-only latch for the refusal to resolve keys at an armed height with no
+ * D2 HARDENING (b): the once-only latch for the refusal to resolve keys at an armed height with no
* height-to-registry binding. One line per block at a 523 ms period is a hazard, not a diagnostic.
*/
private final AtomicBoolean loggedUnboundArmedHeight = new AtomicBoolean(false);
/**
- * AERE COVERAGE MARGIN (2026-08-02): the last coverage situation this node reported, as "N/R", so the
+ * AERE D-078 (2026-08-02): the last coverage situation this node reported, as "N/R", so the
* coverage line is emitted exactly when the situation CHANGES and not once per block. Null means
* nothing has been reported yet.
*/
@@ -467,7 +472,7 @@ public final class FalconSealSupport {
manifestPath);
}
} else if (registryPath != null) {
- // AERE GENESIS BINDING REPAIR (2026-08-02): the properties source the design prescribed was measured to
+ // AERE A8 REPAIR (2026-08-02): the properties source the design prescribed was measured to
// HALT EVERY NODE at the activation height. Not by being mutable, and not by disagreeing with
// a hash: by carrying public keys and NO ADDRESSES. addressForIndex() then returns null for
// every index, and the V2 seals rule (R2) refuses every header that carries a certificate.
@@ -515,13 +520,13 @@ public final class FalconSealSupport {
}
// Address binding is ALL-OR-NOTHING. A half-bound registry is worse than an unbound one:
// some indices would resolve and some would not, so the fleet would reject a header that
- // one operator can verify and another cannot, which is the same defect wearing a different hat.
+ // one operator can verify and another cannot, which is defect A8 wearing a different hat.
final boolean fullyBound = !reg.isEmpty() && propAddr.keySet().equals(reg.keySet());
if (fullyBound) {
regAddr.putAll(propAddr);
} else if (!propAddr.isEmpty()) {
LOG.error(
- "AERE PQC GENESIS-BINDING: LEGACY registry file {} carries {} '.addr' rows for {} keys. A "
+ "AERE PQC A8: LEGACY registry file {} carries {} '.addr' rows for {} keys. A "
+ "PARTIALLY address-bound registry is refused as a binding source (all-or-"
+ "nothing): some indices would resolve to a validator address and some would "
+ "not, so two nodes would disagree about which headers are valid. Treating this "
@@ -589,7 +594,7 @@ public final class FalconSealSupport {
this.pendingLateHash = lateHash;
this.anchorContractAddress = anchorAddr;
- // AERE GENESIS BINDING: remember WHICH file the registry came from, so the consensus-binding guard hashes
+ // AERE A8: remember WHICH file the registry came from, so the consensus-binding guard hashes
// the file this node is really running on and not a different one that happens to be nearby.
if (genesisPath != null) {
this.registrySourcePath = genesisPath;
@@ -628,7 +633,7 @@ public final class FalconSealSupport {
// before anything else. A present-but-unparseable aere.falcon.forkBlock must not silently
// degrade to never-blocking (log-only); it aborts node init here (config time, NOT the per-block
// consensus path).
- // AERE LATE-ANCHOR HEIGHT: the result is TAKEN, not merely checked. Nothing re-reads the property afterwards.
+ // AERE D-079: the result is TAKEN, not merely checked. Nothing re-reads the property afterwards.
this.forkBlock = validateForkBlockConfigOrAbort();
// AERE FIX-OPRIRE-CONSENS (b): resolve and validate the ATTACHMENT gate. Every inconsistent
@@ -636,7 +641,7 @@ public final class FalconSealSupport {
// surprise mid-chain.
this.attachBlock = validateAndResolveAttachBlockOrAbort(this.forkBlock);
- // AERE LATE-ANCHOR HEIGHT: the ORDER between the blocking height and the height at which the registry that
+ // AERE D-079: the ORDER between the blocking height and the height at which the registry that
// backs it can become active. Aborts here, at config time, for the same reason as everything
// above it: after the node has joined, the same error is a silent degradation to log-only.
this.anchorObserveBlock =
@@ -650,7 +655,7 @@ public final class FalconSealSupport {
// is still empty until activation, which is expected.)
armingReadinessDiagnostic();
- // AERE ROW BINDING (2026-08-06): the SECOND arming question, and the one AERE-PQC-REG-ARM-01 cannot
+ // AERE D-146 (2026-08-06): the SECOND arming question, and the one AERE-PQC-REG-ARM-01 cannot
// answer. Address-bound says every index has SOME address next to it. It does not say that the
// validator at that address ever held the Falcon key filed under it. Measured on the real
// verification path on 2026-08-06: a registry with two rows' public keys swapped - no duplicate
@@ -658,7 +663,7 @@ public final class FalconSealSupport {
// header, and one key placed at two indices satisfied a threshold of two on its own.
requireRegistryBindingProofsOrAbort();
- // AERE COVERAGE MARGIN (2026-08-03): the arm-time comparison the repair above NAMED and did not make.
+ // AERE D-078 (2026-08-03): the arm-time comparison the repair above NAMED and did not make.
// armingReadinessDiagnostic() answers "is the manifest address-bound"; it never asks whether the
// threshold the fleet is about to arm is one the fleet can be guaranteed to MEET.
validateThresholdReachabilityOrAbort();
@@ -674,7 +679,7 @@ public final class FalconSealSupport {
* validation, so it never throws on the consensus path. An UNSET value keeps the safe
* never-blocking log-only default.
*
- * AERE LATE-ANCHOR HEIGHT (2026-08-03): it now RETURNS the validated height and the constructor keeps it in
+ * AERE D-079 (2026-08-03): it now RETURNS the validated height and the constructor keeps it in
* a final field. Validating a value and then re-reading its source on every use leaves the
* original defect intact one level down, which is exactly where it was found.
*
@@ -712,7 +717,7 @@ public final class FalconSealSupport {
}
/**
- * AERE LATE-ANCHOR HEIGHT: the ordering guard between the BLOCKING height and the height at which the registry
+ * AERE D-079: the ordering guard between the BLOCKING height and the height at which the registry
* that backs it can first be active.
*
* THE HOLE THIS CLOSES, in the words of the code that documented it and did nothing about it.
@@ -873,7 +878,7 @@ public final class FalconSealSupport {
* {@code aere.falcon.testnetAllowSmallFleet=true}, which logs an ERROR every start.
*
*
- * @param fork the already-validated blocking height (AERE LATE-ANCHOR HEIGHT: passed in rather than re-parsed
+ * @param fork the already-validated blocking height (AERE D-079: passed in rather than re-parsed
* from the property, so this method and {@link #forkBlock()} cannot disagree about it)
* @return the resolved attachment height, or {@link Long#MAX_VALUE} when unset
*/
@@ -1480,9 +1485,8 @@ public final class FalconSealSupport {
* AERE_FALCON_ATTACHINTERVAL}.
*
* WHY THIS EXISTS, measured on chain 2800 on 2026-08-08. Attachment was armed on all seven
- * validators and the header went from 525 to 3844 bytes, five Falcon seals on EVERY block: more
- * than seven times the header bytes, on nodes whose free space could not absorb it. The anchor
- * producer
+ * validators and the header went from 525 to 3844 bytes, five Falcon seals on EVERY block, about
+ * 200 GB per node per year against 12 GB of free disk on the tightest host. The anchor producer
* already has both an interval and a seal cap, but the assembler reached when the anchor is NOT
* armed has neither, and that assembler is the one that runs before the activation height. So the
* cheap-by-design path was unreachable precisely during the window it was needed.
@@ -1665,10 +1669,10 @@ public final class FalconSealSupport {
return chainRelativeState;
}
- // ---- AERE GENESIS BINDING (2026-08-01): BIND THE REGISTRY TO CONSENSUS ----
+ // ---- AERE A8 (2026-08-01): BIND THE REGISTRY TO CONSENSUS ----
/**
- * GENESIS BINDING GUARD: refuse to start when the Falcon registry this node loaded is not the one the chain
+ * A8 GUARD: refuse to start when the Falcon registry this node loaded is not the one the chain
* requires at this height.
*
* THE DEFECT, read out of the constructor above and not guessed at. The registry that answers
@@ -1712,7 +1716,7 @@ public final class FalconSealSupport {
final PqRegistryHash.Schedule schedule;
if (genesisForSchedule == null) {
LOG.warn(
- "AERE PQC GENESIS-BINDING: no genesis file is reachable to read config.pqRegistryHash from (neither "
+ "AERE PQC A8: no genesis file is reachable to read config.pqRegistryHash from (neither "
+ "aere.pq.genesis nor aere.falcon.genesis is set), so the Falcon registry on this "
+ "node is NOT bound to consensus. The registry in use is {} ({}). Two nodes holding "
+ "different registry files would disagree about which public key validator index i "
@@ -1728,7 +1732,7 @@ public final class FalconSealSupport {
}
/**
- * AERE GENESIS BINDING, the form the caller should actually use: verify the registry binding against a
+ * AERE A8, the form the caller should actually use: verify the registry binding against a
* schedule that came from the genesis configuration BESU ITSELF PARSED AND BOOTED WITH.
*
* WHY THIS OVERLOAD EXISTS, and it is not tidiness. The single-argument form above reads
@@ -1754,11 +1758,11 @@ public final class FalconSealSupport {
loaded = PqRegistryHash.loadAuto(Paths.get(registrySourcePath));
}
- // HEIGHT SCHEDULE: build the height-resolved set BEFORE anything is published, from the registry this
+ // D-081: build the height-resolved set BEFORE anything is published, from the registry this
// node signs with plus every registry named in the history list. With no history configured the
- // set holds exactly the one registry and every answer below is what it was before the height schedule existed.
+ // set holds exactly the one registry and every answer below is what it was before D-081.
//
- // AERE HELD-SET SCOPE (2026-08-06). THIS BLOCK USED TO SIT 41 LINES LOWER, AND THAT WAS THE DEFECT. The
+ // AERE D-A (2026-08-06). THIS BLOCK USED TO SIT 41 LINES LOWER, AND THAT WAS THE DEFECT. The
// startup guard below was handed the single primary registry and threw
// AERE-PQC-REG-MISMATCH-01 before this code ever ran, so a node holding exactly the right files
// - the post-rotation registry as its own, the pre-rotation one as history - refused to start
@@ -1799,7 +1803,7 @@ public final class FalconSealSupport {
registryOverrideEngaged = true;
LOG.error(
"AERE PQC EMERGENCY [AERE-PQC-REG-UNSAFE-01]: STARTING ANYWAY WITH AN UNVERIFIED FALCON "
- + "REGISTRY. The genesis-binding guard REFUSED this node ({}), and {} is set, so "
+ + "REGISTRY. The A8 registry-binding guard REFUSED this node ({}), and {} is set, so "
+ "the refusal has been overridden BY EXPLICIT OPERATOR REQUEST. What that means, "
+ "stated plainly: from the first height at which genesis requires a registry hash, "
+ "this node cannot correctly decide whether a header's Falcon certificate is valid, "
@@ -1819,7 +1823,7 @@ public final class FalconSealSupport {
// refuses at exactly the heights that are uncovered, which is the narrowest fail-closed
// action that still names the problem. What must never happen is silence.
LOG.error(
- "AERE PQC HEIGHT-SCHEDULE: this node holds {} registry file(s) and the chain's pqRegistryHash "
+ "AERE PQC D-081: this node holds {} registry file(s) and the chain's pqRegistryHash "
+ "schedule has {} entr(ies), of which the heights {} are covered by NOTHING this "
+ "node holds. Every header at or after such a height will be REFUSED, and a node "
+ "syncing from genesis will stop there. Name the missing registry file(s) in {} "
@@ -1844,13 +1848,13 @@ public final class FalconSealSupport {
}
/**
- * AERE SIGNED-HEIGHT CHECK (2026-08-06), THE LOCAL HALF. The height this node ARMS at must be a height the fleet
+ * AERE D-B (2026-08-06), THE LOCAL HALF. The height this node ARMS at must be a height the fleet
* actually signed a registry for.
*
* WHY THIS CANNOT BE THE SAME KIND OF GUARD AS THE ONE ABOVE, and the difference is the whole
- * honest limitation of that check. {@code aere.pq.anchorBlock} is a SYSTEM PROPERTY, set per node through
- * {@code BESU_OPTS}. It is in no genesis. Nothing in the document the seven validators hold
- * byte-identically constrains it. So the agreement of seven nodes on an arming height CANNOT BE
+ * honest limitation of D-B. {@code aere.pq.anchorBlock} is a SYSTEM PROPERTY, set per node through
+ * {@code BESU_OPTS}. It is in no genesis. Nothing in the document the validators hold
+ * byte-identically constrains it. So the agreement of the nodes on an arming height CANNOT BE
* ENFORCED by any code that runs on one node - it can only be DETECTED locally, and that is what
* this does. Enforcement would require the height to move into genesis, which is a change to the
* chain's configuration and not to this class. That is written here as a limitation, not as a
@@ -1901,7 +1905,7 @@ public final class FalconSealSupport {
}
throw new PqRegistryHash.RegistryConfigException(
"AERE-PQC-REG-BIND-09",
- "AERE PQC SIGNED-HEIGHT: REFUSING TO START - this node arms at a height no validator signed for.\n"
+ "AERE PQC D-B: REFUSING TO START - this node arms at a height no validator signed for.\n"
+ " FIELD: aere.pq.anchorBlock (system property, per node, read through "
+ "PqAnchorConfig) versus the 'block' values of config.pqRegistryHash in genesis ("
+ schedule.source()
@@ -1919,30 +1923,30 @@ public final class FalconSealSupport {
+ "one the fleet signed, the day the chain starts enforcing post-quantum seals is a "
+ "number one operator typed.\n"
+ " WHY NOTHING ELSE CATCHES IT: aere.pq.anchorBlock is not in genesis. It is not in "
- + "the document all seven nodes hold identically, so no node can hold another node to "
+ + "the document every node holds identically, so no node can hold another node to "
+ "it. This refusal is DETECTION on this node only. Two nodes with different "
+ "aere.pq.anchorBlock still do not disagree about a header until the lower of the two "
+ "heights, and this guard cannot see the other node's value.\n"
- + " WHAT TO DO: EITHER set aere.pq.anchorBlock to one of the heights above, on all "
- + "seven nodes and in the same change - a node armed alone validates differently from "
+ + " WHAT TO DO: EITHER set aere.pq.anchorBlock to one of the heights above, on every "
+ + "node and in the same change - a node armed alone validates differently from "
+ "the rest. OR, if the activation day genuinely moved, re-run the key ceremony for the "
+ "new height, put the re-signed registry's NEW hash in config.pqRegistryHash at that "
+ "height, and roll it to the whole fleet. Moving the day is 14 signatures. It is "
+ "supposed to be.\n"
- + " CHECK THE WHOLE FLEET BEFORE RESTARTING ANYTHING: this property has to read "
- + "the same on every node, and a split state is a fault, "
- + "not a warning.");
+ + " CHECK THE WHOLE FLEET BEFORE RESTARTING ANYTHING: this property is per node and "
+ + "is in no genesis, so two nodes can disagree about it silently. Compare it across "
+ + "every validator and treat a split state as a fault, not as a detail.");
}
/**
- * AERE GENESIS BINDING, the PER-BLOCK half: does the registry this node is running on satisfy the binding the
+ * AERE A8, the PER-BLOCK half: does the registry this node is running on satisfy the binding the
* chain requires AT THIS HEIGHT? Never throws.
*
* WHY A STARTUP GUARD IS NOT ENOUGH, measured as a gap and not assumed. The startup guard
* answers the question once, against the chain head that existed at startup. A node that is
* already running when a ROTATION height in the schedule passes underneath it is never asked
* again: it keeps validating with a registry the chain has moved off, and it keeps reporting
- * itself healthy while doing it. That is the same failure mode the genesis binding was opened for, arriving by a
+ * itself healthy while doing it. That is the same failure mode A8 was opened for, arriving by a
* different door. With a schedule of one entry the two guards are equivalent; with two or more,
* only this one covers the interval after the second entry.
*
@@ -1966,7 +1970,7 @@ public final class FalconSealSupport {
return true; // below the first scheduled height: the 11.8 million existing blocks are untouched
}
final PqRegistryHash.Registry loaded = this.registryBindingLoaded;
- // HEIGHT SCHEDULE: ask the whole scheduled history, not only the entry in force at the head. Before this
+ // D-081: ask the whole scheduled history, not only the entry in force at the head. Before this
// change a node held one registry, so one rotation left NO configuration that satisfied both
// the pre-rotation interval and the head, and the chain became permanently unjoinable.
final boolean ok =
@@ -2029,11 +2033,10 @@ public final class FalconSealSupport {
blockNumber,
required.hash(),
required.block(),
- // GENESIS BINDING: this used to print hashV1 next to `required.hash()`, which is computed
- // with hashFor. Two numbers set side by side to be compared, but computed differently: for
- // a registry that carries proofs the two can NEVER match, and the message sends the
- // operator hunting for the defect where it is not. Measured on a fleet of seven on
- // 2026-08-06.
+ // A8: this used to print hashV1 next to `required.hash()`, which is computed with hashFor.
+ // Two numbers set side by side to be compared, but computed differently: for a registry
+ // that carries proofs the two can NEVER match, and the message sends the operator hunting
+ // for the defect where it is not. Measured on a fleet of seven on 2026-08-06.
loaded == null
? "(no registry loaded)"
: PqRegistryHash.hashFor(loaded, registryBindingChainId),
@@ -2058,7 +2061,7 @@ public final class FalconSealSupport {
return;
}
LOG.error(
- "AERE PQC GENESIS-BINDING [AERE-PQC-REG-BLOCK-01]: REFUSING header at height {} (fail-closed). The "
+ "AERE PQC A8 [AERE-PQC-REG-BLOCK-01]: REFUSING header at height {} (fail-closed). The "
+ "chain requires registry hash {} from height {} (schedule source {}), and the "
+ "registry this node is running has hash {}. Registry in use: {} ({}), {} entries, "
+ "address-bound={}. WHY THIS FIRES NOW AND NOT AT STARTUP: this node started under an "
@@ -2069,8 +2072,8 @@ public final class FalconSealSupport {
required.hash(),
required.block(),
this.registryBindingSchedule == null ? "(none)" : this.registryBindingSchedule.source(),
- // GENESIS BINDING, same reason as above: this is compared against required.hash(), which
- // is computed with hashFor.
+ // A8, same reason as above: this is compared against required.hash(), which is computed
+ // with hashFor.
loaded == null ? "(no registry loaded)" : PqRegistryHash.hashFor(loaded, registryBindingChainId),
loaded == null ? "(none)" : loaded.sourcePath(),
loaded == null ? "(none)" : loaded.kind(),
@@ -2095,7 +2098,7 @@ public final class FalconSealSupport {
}
/**
- * Outcome of the genesis-binding guard.
+ * Outcome of the A8 registry-binding guard.
*
* @return the gate state, or null when the guard has not run
*/
@@ -2312,9 +2315,9 @@ public final class FalconSealSupport {
}
final Iterator Nothing else catches it. Not startup, not the log, not the block rhythm, not rejections, not
* the divergence detector - every node agrees, because the certificate is valid. Until today the
* only guard was a PROCEDURE step run by hand on each target, and a procedure step can be skipped.
- * The written procedure claimed it "shows up as R2 rejections at the anchor height"; measured, it does not.
+ * The runbook claimed it "shows up as R2 rejections at the anchor height"; measured, it does not.
*
* WHY IT IS SAFE TO REFUSE HERE. Same argument as the other startup guards: this runs before
* the network and the QBFT state machine start, so it is a clean refusal to start rather than a
* mid-flight halt. It is INERT unless a Falcon key is configured AND the registry is address-bound,
- * so wherever no {@code aere.falcon.key} is configured, it cannot fire.
+ * so on chain 2800 as it stands today - no {@code aere.falcon.key} anywhere - it cannot fire.
*
* A null bound address is NOT treated as a failure: that means a pubkey-only registry, which is
* already refused elsewhere for the paths that matter, and turning it into a second refusal here
@@ -2600,16 +2648,16 @@ public final class FalconSealSupport {
}
/**
- * LOOKUP HARDENING (a). THIS NODE's own signing identity, which is the one place in the stack that
+ * D2 HARDENING (a). THIS NODE's own signing identity, which is the one place in the stack that
* legitimately has no height.
*
* WHY IT IS A SEPARATE METHOD AND NOT {@code addressForIndex(localIndex())}. The adversarial
- * review of 2026-08-02 found the verification path asking the registry questions with no
+ * review of 2026-08-02 (D2) found the verification path asking the registry questions with no
* height. Deleting the height-less pair from {@link PqSignerRegistry} left exactly one honest
* caller behind: {@code QbftBlockCreatorAdaptor} asking "am I, right now, an eligible signer",
* before signing with the single private key this process holds. There is no historical question
* in that, and inventing a height for it would be a lie dressed as rigour. Giving it its own name
- * means the two uses can no longer be confused by a future reader, which is how the defect arrived in the
+ * means the two uses can no longer be confused by a future reader, which is how D2 arrived in the
* first place.
*
* @return this node's registry-bound address, or null when it holds no key or the registry is not
@@ -2620,16 +2668,16 @@ public final class FalconSealSupport {
}
/**
- * LOOKUP HARDENING (b). The height at and above which this node is ARMED, i.e. from which a header's
+ * D2 HARDENING (b). The height at and above which this node is ARMED, i.e. from which a header's
* Falcon certificate carries consensus weight.
*
* WHY THE HEIGHT-RESOLVED LOOKUPS NEED IT. Before this, {@code keyAt} and {@code
* addressForIndexAt} fell back to the HEAD registry whenever no {@code config.pqRegistryHash}
* entry was in force - and {@code pqRegistryHash} is in no genesis this fleet runs (measured
- * 2026-08-05: a search for {@code pqRegistryHash} across the deployment configuration returns
- * nothing). So the whole height-schedule machinery was inert and the answer above the arming
- * height was still "verify this year-old header against today's keys", which IS the height-less
- * lookup defect, unrepaired. Falling back is correct BELOW the arming height, where no certificate is being judged; at and above it,
+ * 2026-08-05: {@code grep -rn pqRegistryHash --include=*.json deploy/ monitoring/} returns
+ * nothing). So the whole D-081 machinery was inert and the answer above the arming height was
+ * still "verify this year-old header against today's keys", which IS D2/T2, unrepaired. Falling
+ * back is correct BELOW the arming height, where no certificate is being judged; at and above it,
* the honest answer to "which keys were in force here" is a refusal, not a guess.
*
* Read through {@link PqAnchorProducer#config()} so the value is the same one the producer and
@@ -2708,13 +2756,21 @@ public final class FalconSealSupport {
}
/**
- * The block number at and after which the Falcon quorum certificate becomes BLOCKING (a valid
- * >= 2f+1 Falcon quorum is required for a block to be accepted). Before this block the Falcon
- * seals are additive / log-only. Configured via {@code aere.falcon.forkBlock} / {@code
- * AERE_FALCON_FORKBLOCK}; defaults to {@link Long#MAX_VALUE} (never blocking, pure log-only) so
- * an unconfigured node behaves exactly like the additive baseline.
+ * The block number at and after which the LEGACY per-block Falcon rule
+ * ({@link org.hyperledger.besu.consensus.qbft.headervalidationrules.FalconSealValidationRule})
+ * becomes blocking. Configured via {@code aere.falcon.forkBlock} / {@code AERE_FALCON_FORKBLOCK};
+ * defaults to {@link Long#MAX_VALUE} (never blocking, pure log-only) so an unconfigured node
+ * behaves exactly like the additive baseline.
*
- * AERE LATE-ANCHOR HEIGHT (2026-08-03): this used to re-read the property on every call and swallow a
+ * ON CHAIN 2800 THIS ARMS A RULE THAT IS ALREADY RETIRED, so setting it changes nothing
+ * (finding D-235, corrected 2026-08-19). The legacy rule stands down at
+ * {@code PqAnchorConfig.legacyFalconRuleRetirementBlock()} = the anchor block, 13,014,000, and
+ * this property is set to 14,050,000 - above it. Until 2026-08-19 our own public texts said a
+ * per-block 2f+1 Falcon quorum had been blocking since that height; the claim was withdrawn the
+ * same day. What is defensible: at every 32nd height a certificate of at least K valid Falcon-512
+ * seals sits under the block hash, and without it that block does not finalize.
+ *
+ * AERE D-079 (2026-08-03): this used to re-read the property on every call and swallow a
* NumberFormatException into {@link Long#MAX_VALUE} with a WARN line. That is the finding, in one
* method: the single value the entire post-quantum enforcement layer is gated on could silently
* become "never blocking", and the only trace was a log line nothing reads. The value is now
@@ -2729,7 +2785,7 @@ public final class FalconSealSupport {
}
/**
- * AERE LATE-ANCHOR HEIGHT: the DECLARED height at or after which the on-chain late-anchor registry contract is
+ * AERE D-079: the DECLARED height at or after which the on-chain late-anchor registry contract is
* expected to be observable, or {@link Long#MAX_VALUE} when undeclared.
*
* @return the declared anchor observation height
@@ -2739,7 +2795,7 @@ public final class FalconSealSupport {
}
/**
- * AERE LATE-ANCHOR HEIGHT: the first height at which this node validated a header at or after the blocking
+ * AERE D-079: the first height at which this node validated a header at or after the blocking
* height while the anchored registry was NOT active, or -1 if that has never happened.
*
* The configuration guard refuses the misconfiguration that CAUSES this. It cannot refuse the
@@ -2806,7 +2862,7 @@ public final class FalconSealSupport {
fork);
return;
}
- // AERE GENESIS BINDING REPAIR (2026-08-02). This used to be LOG.error and nothing else, and the comment
+ // AERE A8 REPAIR (2026-08-02). This used to be LOG.error and nothing else, and the comment
// above still says "diagnostic only". That was measured to be the wrong trade. A node that
// starts here does not stay harmless: it joins the fleet, reaches the activation height, and
// from that height rejects every header that carries a certificate. Measured on three
@@ -2822,12 +2878,12 @@ public final class FalconSealSupport {
// that this class never throws is a promise about the PER-BLOCK path, and it is kept: nothing
// below this constructor throws.
//
- // The refusal is inert wherever forkBlock is unset, because
+ // The refusal is inert on chain 2800 as it stands today, because forkBlock is unset there, so
// this code cannot stop a live validator that is running now.
throw new ActivationConfigException(
ActivationConfigException.Kind.UNSAFE,
"AERE-PQC-REG-ARM-01",
- "AERE PQC GENESIS-BINDING: REFUSING TO START (fail-closed). Falcon blocking is "
+ "AERE PQC A8: REFUSING TO START (fail-closed). Falcon blocking is "
+ "configured at forkBlock="
+ fork
+ " but the active Falcon registry is NOT ADDRESS-BOUND"
@@ -2855,7 +2911,7 @@ public final class FalconSealSupport {
}
/**
- * AERE ROW BINDING (2026-08-06). REFUSE TO START when this node is ARMED and the registry it will be
+ * AERE D-146 (2026-08-06). REFUSE TO START when this node is ARMED and the registry it will be
* held to carries no binding proofs.
*
* WHAT IT ADDS OVER {@code AERE-PQC-REG-ARM-01}. That guard asks whether the registry is
@@ -2872,23 +2928,23 @@ public final class FalconSealSupport {
* signature by the validator repairs ATTRIBUTION.
*
* WHEN IT FIRES. Only when this node is armed: {@code aere.falcon.forkBlock} is configured, or
- * the certificate anchor is active per {@link #anchorArmedFrom()}. Both are unset as it
- * stands, so this refusal is INERT on the live fleet exactly as AERE-PQC-REG-ARM-01 is, and it
+ * the certificate anchor is active per {@link #anchorArmedFrom()}. Both are unset on chain 2800 as
+ * it stands, so this refusal is INERT on the live fleet exactly as AERE-PQC-REG-ARM-01 is, and it
* cannot stop a validator that is running today. That inertness is not asserted here, it is
* measured by {@code PqInertBinaryTest}, which drives a node with no {@code aere.pq.*}
* and no {@code aere.falcon.forkBlock} property at all through this constructor.
*
* WHAT IT DOES NOT JUDGE. A node armed with NO registry file at all returns without a word.
- * Row binding is about a row that credits a seal to the wrong validator, and a registry with no rows
+ * D-146 is about a row that credits a seal to the wrong validator, and a registry with no rows
* credits nobody; that condition belongs to AERE-PQC-REG-ARM-01 and AERE-PQC-CFG-UNSAFE-08, which
* own it and name the numbers. See the comment at the return itself for what was measured when
* this guard tried to own it too.
*
* WHY IT IS NOT BEHIND THE EMERGENCY BYPASS. {@code aere.falcon.registry.mismatch.allow}
- * overrides the genesis hash gate, which answers "is this the file genesis named" - a question about
+ * overrides the A8 hash gate, which answers "is this the file genesis named" - a question about
* configuration drift, recoverable by installing the right file. This one answers "can this
* registry be trusted to say who signed", and the anchor contract is IMMUTABLE once written: a
- * fleet armed over unbound rows carries that defect for the life of the chain. The way out of THIS one
+ * fleet armed over unbound rows carries D-146 for the life of the chain. The way out of THIS one
* is the same as for AERE-PQC-REG-ARM-01 - do not arm, or disarm: unset {@code
* aere.falcon.forkBlock}, or set {@code aere.pq.anchor.disable=true}, which makes {@link
* #anchorArmedFrom()} return {@link Long#MAX_VALUE} and leaves {@code PqEmergencyShoutRule}
@@ -2911,11 +2967,11 @@ public final class FalconSealSupport {
*/
private void requireRegistryBindingProofsOrAbort() {
if (forkBlock() == Long.MAX_VALUE && anchorArmedFrom() == Long.MAX_VALUE) {
- return; // nothing armed on this node; inert
+ return; // nothing armed on this node; inert, exactly as on chain 2800 today
}
if (registrySourcePath == null) {
- // AN ARMED NODE WITH NO REGISTRY AT ALL IS NOT A ROW-BINDING DEFECT, and this return is the
- // difference between a guard and a blanket. Row binding is about mis-ATTRIBUTION: a row that credits a
+ // AN ARMED NODE WITH NO REGISTRY AT ALL IS NOT A D-146 DEFECT, and this return is the
+ // difference between a guard and a blanket. D-146 is mis-ATTRIBUTION: a row that credits a
// seal to a validator who never held the key on it. That requires ROWS. With no registry
// there are no rows, indexToAddress is empty, the eligible-signer set is empty, and nobody
// can be credited with anything - there is nothing for a forged binding to say.
@@ -2943,7 +2999,7 @@ public final class FalconSealSupport {
}
/**
- * AERE COVERAGE MARGIN (2026-08-03). REFUSE TO START when the armed Falcon threshold K is above the number of
+ * AERE D-078 (2026-08-03). REFUSE TO START when the armed Falcon threshold K is above the number of
* anchored-key holders this fleet is GUARANTEED to have among a block's ECDSA committers.
*
* WHY THIS EXISTS. The 2026-08-02 repair correctly removed the fleet-wide coverage question from
@@ -2961,16 +3017,16 @@ public final class FalconSealSupport {
* it can only LOWER K later, and a guard must not be satisfied by a lever an operator may not pull.
*
* WHY IT MAY THROW HERE. Same reasoning, same path and same precedent as AERE-PQC-CFG-UNSAFE-04
- * and the genesis-binding refusal directly above: this is the constructor, the network is not up, the QBFT state
+ * and the A8 refusal directly above: this is the constructor, the network is not up, the QBFT state
* machine does not exist, and no header has been offered to anyone. The promise that this class
* never throws is a promise about the PER-BLOCK path, and it is kept.
*
* WHY IT IS NOT A RUNTIME CHECK. A per-block version of this comparison would be exactly the
- * defect rebuilt: a fleet-wide fact, false on every node at the same height after an
+ * defect of D-078 rebuilt: a fleet-wide fact, false on every node at the same height after an
* ordinary vote, answered with a halting action. The runtime half stays a REPORT, in {@link
* #reportRegistryCoverage}, and decides nothing.
*
- * INERT WHERE {@code aere.pq.anchorBlock} IS UNSET: there is no
+ * INERT ON CHAIN 2800 as it stands: {@code aere.pq.anchorBlock} is unset there, so there is no
* threshold and no comparison. This code cannot stop a validator that is running now.
*/
private void validateThresholdReachabilityOrAbort() {
@@ -3034,7 +3090,7 @@ public final class FalconSealSupport {
throw new ActivationConfigException(
ActivationConfigException.Kind.UNSAFE,
"AERE-PQC-CFG-UNSAFE-08",
- "AERE PQC COVERAGE: REFUSING TO START (fail-closed). The Falcon anchor is armed at height "
+ "AERE PQC D-078: REFUSING TO START (fail-closed). The Falcon anchor is armed at height "
+ anchor.anchorBlock()
+ " with a staged threshold that reaches K="
+ k
@@ -3052,7 +3108,7 @@ public final class FalconSealSupport {
+ ". WHAT THIS MEANS: with every node honest and every node up, a proposer can fail to "
+ "assemble a certificate, and it fails on every node at once because the validator set "
+ "is consensus state - the chain stops in a state where the re-anchoring transaction "
- + "that would repair it can no longer be carried by any block. That is an unreachable threshold. WHAT TO "
+ + "that would repair it can no longer be carried by any block. That is D-078. WHAT TO "
+ "DO: re-anchor the manifest so that every validator holds a key (this is what makes "
+ "the standing 'grow to N>=9 before arming' order safe - the manifest has to grow WITH "
+ "the set, not after it), or lower the "
@@ -3064,7 +3120,7 @@ public final class FalconSealSupport {
}
/**
- * AERE COVERAGE MARGIN: how many validators hold an anchored Falcon key, counting a late anchor that has not
+ * AERE D-078: how many validators hold an anchored Falcon key, counting a late anchor that has not
* landed yet the same way {@link #expectedFleetSize()} does, so the two numbers being compared are
* read at the same moment from the same manifest.
*
@@ -3112,9 +3168,9 @@ public final class FalconSealSupport {
}
/**
- * AERE COVERAGE MARGIN: the height at which a validator set was last observed, or {@code -1} when none ever
+ * AERE D-078: the height at which a validator set was last observed, or {@code -1} when none ever
* was. Diagnostic, and the only way a test can tell "the gate is being fed" from "the gate happens
- * to say yes anyway", which is the difference the coverage repair turns on.
+ * to say yes anyway", which is the difference the D-078 repair turns on.
*
* @return the observation height, or -1 if no validator set has been observed
*/
@@ -3134,7 +3190,7 @@ public final class FalconSealSupport {
* AERE COVERAGE MARGIN (2026-08-02): condition 3 used to be "that registry COVERS every validator in
+ * AERE D-078 (2026-08-02): condition 3 used to be "that registry COVERS every validator in
* the last observed validator set", and that is the sentence that stopped the chain. Coverage
* is a property of the validator set, which is consensus state, so an ordinary add-validator vote
* falsifies it on every node at the same height; the old answer to that was to switch attachment
@@ -3160,7 +3216,7 @@ public final class FalconSealSupport {
* cannot check peer BINARY VERSION, and it is emphatically NOT what makes the halting order
* impossible. Until 2026-08-02 it asked the FLEET question instead - does the registry cover
* every observed validator - and switched attachment off when the answer was no; see the
- * coverage note on the method below for why that was a chain stop rather than a safeguard.
+ * D-078 note on the method below for why that was a chain stop rather than a safeguard.
* A block needs {@code ceil(2N/3)} ECDSA committed seals. An adversarial (or merely unlucky)
* choice of committers takes every unkeyed validator first, so the guaranteed keyed count is
* {@code quorum - (N - keyed)}, floored at zero. A Falcon threshold K above this number is a
- * threshold the chain is not guaranteed to be able to meet, which is exactly the shape of that halt:
+ * threshold the chain is not guaranteed to be able to meet, which is exactly the shape of D-078:
* at N=7 with all 7 keyed and K=5 the margin is exactly zero, one added unkeyed validator holds it
* at zero, and a second takes it negative.
*
@@ -3355,7 +3411,7 @@ public final class FalconSealSupport {
}
/**
- * AERE COVERAGE MARGIN (2026-08-03). By how much a Falcon threshold {@code k} exceeds what the fleet is
+ * AERE D-078 (2026-08-03). By how much a Falcon threshold {@code k} exceeds what the fleet is
* GUARANTEED to be able to produce. Zero means reachable; any positive number is a threshold a
* proposer can legitimately fail to meet with every node honest and every node up.
*
@@ -3393,15 +3449,131 @@ public final class FalconSealSupport {
if (!attachmentArmed(blockNumber)) {
return Optional.empty();
}
- try {
- final FalconSigner signer = new FalconSigner();
- signer.init(true, localPrivateKey);
- final byte[] sig = signer.generateSignature(commitHash.toArray());
- return Optional.of(new FalconSeal(localIndex, Bytes.wrap(sig)));
- } catch (final RuntimeException e) {
- LOG.warn("AERE PQC: Falcon signing failed (ECDSA seal unaffected): {}", e.toString());
+ // AERE AGILITY step 4 (2026-08-24): signing goes through the scheme layer. The same
+ // FalconSigner underneath, but the path is now the one the hybrid will use too; the 751
+ // baseline tests prove the equivalence.
+ final java.util.Optional SEPARAT de {@code aere.falcon.attachBlock}, si separarea e obligatorie: daca emiterea pe
+ * PREPARE ar porni odata cu cea pe commit, ridicarea binarului pe flota ar deveni o zi de flag.
+ * Asa, binarul poate sta luni de zile pe toate nodurile inainte ca vreunul sa emita ceva nou.
+ */
+ public static final String PREPARE_ATTACH_PROPERTY = "aere.pq.preparePq.attachBlock";
+
+ /** Environment fallback for {@link #PREPARE_ATTACH_PROPERTY}. */
+ public static final String PREPARE_ATTACH_ENV = "AERE_PQ_PREPAREPQ_ATTACHBLOCK";
+
+ /**
+ * The configured PREPARE attachment height, read fresh on every call.
+ *
+ * Absent = {@link Long#MAX_VALUE}, i.e. never. A value that is PRESENT but unreadable REFUSES
+ * loudly instead of disarming: the lesson paid for by the anchor loader is that a mistyped
+ * character must never boot the node DISARMED, because then nobody finds out.
+ *
+ * @return the height, or Long.MAX_VALUE when unset
+ */
+ public static long prepareAttachBlock() {
+ final String raw = resolve(PREPARE_ATTACH_PROPERTY, PREPARE_ATTACH_ENV);
+ if (raw == null || raw.isBlank()) {
+ return Long.MAX_VALUE;
+ }
+ try {
+ final long v = Long.parseLong(raw.trim());
+ if (v < 0) {
+ throw new NumberFormatException("negative");
+ }
+ return v;
+ } catch (final NumberFormatException e) {
+ throw new ActivationConfigException(
+ ActivationConfigException.Kind.SYNTAX,
+ "AERE-PQC-PREPARE-CONF-01",
+ "AERE PQ PREPARE: "
+ + PREPARE_ATTACH_PROPERTY
+ + " is set to '"
+ + raw
+ + "', which is not a non-negative block height. A node must REFUSE to start rather "
+ + "than silently run with PREPARE attachment disarmed: a disarmed node looks exactly "
+ + "like a correctly configured one until the day it matters.");
+ }
+ }
+
+ /**
+ * Sign this node's own PREPARE, when the PREPARE attachment gate is open at this height.
+ *
+ * It requires THREE things, and each closes one way of being wrong:
+ *
+ * NEVER THROWS except for the strict configuration case above: a signing failure is a log line
+ * and an empty value, exactly as at commit, because the ECDSA path must not be disturbed.
+ *
+ * @param blockNumber the height being prepared
+ * @param message the domain-separated PREPARE message (see PqAnchor.prepareMessage)
+ * @return the seal, or empty when any gate is shut
+ */
+ public Optional It exists so that the COVERAGE step can be measured: without a number, arming enforcement
+ * would be a bet. Read from the log on a testnet, and from this value in tests.
+ *
+ * @return the count
+ */
+ public long preparesSealed() {
+ return preparesSealed.get();
}
/**
@@ -3417,7 +3589,7 @@ public final class FalconSealSupport {
}
/**
- * HEIGHT SCHEDULE. The Falcon public key registered for an index AT A HEIGHT: the one carried by the
+ * D-081. The Falcon public key registered for an index AT A HEIGHT: the one carried by the
* registry the chain's schedule makes active there.
*
* WHY A HEIGHT IS NEEDED AT ALL. A certificate inside a block at height h was produced under
@@ -3429,7 +3601,7 @@ public final class FalconSealSupport {
*
* Returns the head registry's key when no binding is active at that height, which is every
* height below the schedule's first entry and every height on a chain with no schedule at all. So
- * a node with a single registry gets exactly what it got before the height schedule existed.
+ * a node on chain 2800 as it stands today gets exactly what it got before D-081.
*/
private FalconPublicKeyParameters keyAt(
final long blockNumber, final int validatorIndex, final boolean historic) {
@@ -3442,11 +3614,11 @@ public final class FalconSealSupport {
final Optional LOOKUP HARDENING (b-v2): PRIVATE, with the caller's motive as an argument. The two public
+ * D2 HARDENING (b-v2): PRIVATE, with the caller's motive as an argument. The two public
* doors are {@link #addressForIndexAtHistoric} and {@link #addressForIndexAtOwnHead}, each of
* which passes a constant. Nothing outside this file can choose the value, so the one shared
* resolution body cannot drift between the two paths and no caller can pick the wrong flag.
@@ -3512,10 +3684,10 @@ 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.
+ // D-228. 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 WHY THE MOTIVE IS IN THE NAME AND NOT IN THE HEIGHT, measured on 2026-08-06. The first shape
* of hardening (b) refused whenever no height binding existed at or above the arming height, and
* decided that from the block number alone. That stopped the two paths that work on THIS NODE'S
* OWN head: restoring seals from disk after a restart, and proposing. Six tests went red, five in
- * {@code PqSealPersistenceTest} and one in {@code PqForkValidatorSetChangeTest}, and the refusal
+ * {@code PqSealPersistenceTest} and one in {@code PqForkValidatorSetChangeTest}, and the D078
* message says the consequence outright - the node refuses to propose, so it stops producing
* blocks. In every one of the six the number presented to the guard was 1030 against an arming
* height of 1000, identical to what a real historical question would present in the same process
@@ -3565,7 +3737,7 @@ public final class FalconSealSupport {
*
* Reaching THIS method means the node is judging somebody else's claim about a height it did
* not build. "Which keys were in force here" then has an answer that is not this node's current
- * registry, and answering from the head registry anyway is that defect verbatim: a header produced under
+ * registry, and answering from the head registry anyway is D2/T2 verbatim: a header produced under
* one key set checked against another, with success reported.
*
* @param blockNumber the height of the header carrying the seal
@@ -3584,7 +3756,7 @@ public final class FalconSealSupport {
}
/**
- * LOOKUP HARDENING (b-v2). THE OWN-HEAD DOOR. Verify a Falcon seal over a block this node holds as its
+ * D2 HARDENING (b-v2). THE OWN-HEAD DOOR. Verify a Falcon seal over a block this node holds as its
* own head, or is building right now.
*
* It does not refuse for a missing height binding, because at this node's own head the head
@@ -3613,7 +3785,7 @@ public final class FalconSealSupport {
}
/**
- * HEIGHT SCHEDULE / LOOKUP HARDENING (b-v2). THE HISTORY DOOR, address half. See {@link #verifyAtHistoric}.
+ * D-081 / D2 HARDENING (b-v2). THE HISTORY DOOR, address half. See {@link #verifyAtHistoric}.
*
* @param blockNumber the height of the header being validated
* @param validatorIndex the registry index carried by a Falcon seal
@@ -3624,7 +3796,7 @@ public final class FalconSealSupport {
}
/**
- * LOOKUP HARDENING (b-v2). THE OWN-HEAD DOOR, address half. See {@link #verifyAtOwnHead}.
+ * D2 HARDENING (b-v2). THE OWN-HEAD DOOR, address half. See {@link #verifyAtOwnHead}.
*
* @param blockNumber this node's own head, or the block it is building
* @param validatorIndex the registry index
@@ -3635,14 +3807,14 @@ public final class FalconSealSupport {
}
/**
- * LOOKUP HARDENING (b). The one decision the whole hardening turns on, isolated so that removing it is
- * a one-line edit and the negative control can prove the hardening test goes red without it.
+ * D2 HARDENING (b). The one decision the whole hardening turns on, isolated so that removing it is
+ * a one-line edit and the negative control can prove the D2 test goes red without it.
*
* THE MEASURED DEFECT. Both height-resolved lookups used to answer a height they had no binding
* for by returning the HEAD registry - the key set in force right now. Below the arming height
* that is correct and costs nothing: no certificate is being judged there. At and above it, it is
- * the height-less lookup defect verbatim: "an armed node verifies a year-old header against the
- * keys it holds today", so one rotation makes every block between the arming height and the rotation unverifiable, and the
+ * D2/T2 verbatim: "an armed node verifies a year-old header against the keys it holds today", so
+ * one rotation makes every block between the arming height and the rotation unverifiable, and the
* node reports success while doing it. Refusing is the only answer that does not assert a check
* that was not performed.
*
@@ -3653,7 +3825,7 @@ public final class FalconSealSupport {
* config.pqRegistryHash} plus {@code aere.falcon.registry.history} and restart. Compare the
* silence this replaces, where the same node imports the whole chain and calls it verified.
*
- * LOOKUP HARDENING (b-v2), 2026-08-06. The condition gained ONE term, {@code historic}, and
+ * D2 HARDENING (b-v2), 2026-08-06. The condition gained ONE term, {@code historic}, and
* that term is the whole of the second repair. The first shape refused on height alone; the
* six tests it turned red were all asking about this node's OWN head at height 1030 with an
* arming height of 1000, which is the same pair of numbers a genuinely historical question
@@ -3678,11 +3850,11 @@ public final class FalconSealSupport {
}
/**
- * LOOKUP HARDENING (b). The address half of {@link #headRegistryKeyOrRefuse}, with the same rule and
+ * D2 HARDENING (b). The address half of {@link #headRegistryKeyOrRefuse}, with the same rule and
* for the same reason: at and above the arming height an unbound height has no answer, and {@code
* PqAnchorSealsRule} refuses an index it cannot bind to an address rather than skipping it.
*
- * LOOKUP HARDENING (b-v2): same one added term as {@link #headRegistryKeyOrRefuse}.
+ * D2 HARDENING (b-v2): same one added term as {@link #headRegistryKeyOrRefuse}.
*
* @param blockNumber the height being asked about
* @param validatorIndex the registry index
@@ -3704,7 +3876,7 @@ public final class FalconSealSupport {
}
/**
- * LOOKUP HARDENING (b). Say it once per configuration change, not once per block: at a 523 ms block
+ * D2 HARDENING (b). Say it once per configuration change, not once per block: at a 523 ms block
* period a per-block ERROR is itself a hazard on this fleet, and the refusal is already visible as
* a stopped node.
*
@@ -3714,9 +3886,9 @@ public final class FalconSealSupport {
private void shoutUnboundHeight(final long blockNumber, final String why) {
if (loggedUnboundArmedHeight.compareAndSet(false, true)) {
LOG.error(
- "AERE PQC LOOKUP-HARDENING: REFUSING to resolve Falcon keys at height {} - this node is ARMED from {} "
+ "AERE PQC D2: REFUSING to resolve Falcon keys at height {} - this node is ARMED from {} "
+ "and {}. Until 2026-08-06 this fell back to the registry in force at the HEAD, "
- + "which is the height-less lookup defect: a header produced under one key set was checked against "
+ + "which is the D2/T2 defect: a header produced under one key set was checked against "
+ "another, and one key rotation would have made every block above the arming height "
+ "unverifiable while the node reported success. Every header at or above the arming "
+ "height will now be REFUSED until the height-to-registry binding exists. WHAT TO DO: "
@@ -3737,13 +3909,14 @@ public final class FalconSealSupport {
if (pub == null || commitHash == null || signature == null) {
return false;
}
- try {
- final FalconSigner verifier = new FalconSigner();
- verifier.init(false, pub);
- return verifier.verifySignature(commitHash.toArray(), signature.toArray());
- } catch (final RuntimeException e) {
- LOG.debug("AERE PQC: Falcon verify threw for index {}: {}", validatorIndex, e.toString());
- return false;
+ // AERE AGILITY step 4: verification goes through the scheme layer, on the registry form
+ // (raw h, 896 bytes). verifyRaw never throws; a false here is an invalid seal,
+ // exactly the old contract.
+ final boolean valid =
+ SealSchemes.FALCON_512.verifyRaw(pub.getH(), commitHash.toArray(), signature.toArray());
+ if (!valid) {
+ LOG.debug("AERE PQC: Falcon seal did not verify for index {}", validatorIndex);
}
+ return valid;
}
}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducer.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducer.java
new file mode 100644
index 0000000..591ede1
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducer.java
@@ -0,0 +1,140 @@
+/*
+ * AERE HYBRID, the PRODUCER half (2026-08-25). The counterpart of PqCommitEnforcement: that one
+ * decides what is accepted, this one decides what is EMITTED.
+ *
+ * WHY A SEPARATE CLASS FROM FalconSealSupport. Falcon has an old production path, with
+ * per-component loading, startup guards and a singleton; widening it would have meant touching
+ * the very class the live consensus hangs on, for a capability armed nowhere today.
+ * Falcon is not touched here at all: this class produces ONLY the seals of the other schemes,
+ * i.e. exactly the content of the extras slot in CommitPayload.
+ *
+ * THE EMISSION GATE IS WHY THIS CLASS IS ALLOWED TO EXIST. Adding extras changes the signed
+ * bytes, so an older node can no longer PARSE the message. What protects the fleet is not
+ * leniency at decode time, which cannot work, but the fact that nothing emits extras until the
+ * attach height, the same discipline as the Falcon gate. Unset means: never emit,
+ * EVER, and that is the default.
+ *
+ * HALF A CERTIFICATE IS NOT EMITTED. If the schedule requires a scheme this node has no key
+ * for, no maimed certificate is sent (every neighbour would refuse it at quorum anyway):
+ * nothing is sent, and the log SHOUTS. An operator must find out a key is missing
+ * BEFORE the height where enforcement bites, not on that very day.
+ */
+package org.hyperledger.besu.consensus.common.bft;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Produces the non-Falcon scheme seals a hybrid commit carries, gated on height. */
+public final class HybridSealProducer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(HybridSealProducer.class);
+
+ /** The disarmed attachment height: no block ever reaches it, so nothing is ever emitted. */
+ public static final long NEVER = Long.MAX_VALUE;
+
+ private final long attachFromBlock;
+ private final PqSchemeSchedule schedule;
+ private final int validatorIndex;
+ private final Map Never throws: a producer fault must never take down the ECDSA commit path. Every refusal
+ * is a logged reason plus an empty list, exactly the stance of the Falcon signer.
+ *
+ * @param blockNumber the height of the block being committed
+ * @param message the very bytes the Falcon seal of this commit signs
+ * @return the seals, or empty when the gate is shut, a key is missing, or signing failed
+ */
+ public List SEPARATE FROM COMMIT, and the separation is a security requirement, not a matter of style.
+ * If a PREPARE seal signed the same bytes as a commit seal, an adversary could take the PREPARE
+ * dat CINSTIT de un validator si sa il lipeasca pe un COMMIT falsificat: semnatura ar verifica,
+ * si chiar regula pusa sa apere commitul ar fi ocolita. Un singur sir schimbat in preimagine face
+ * cele doua semnaturi netransferabile.
+ */
+ public static final String PREPARE_DOMAIN = "AERE-PQ-PREPARE-1";
+
+ /** The prepare domain label as raw bytes. */
+ public static final Bytes PREPARE_DOMAIN_BYTES =
+ Bytes.wrap(PREPARE_DOMAIN.getBytes(StandardCharsets.US_ASCII));
+
/** Orders Falcon seals by their registry index, ascending. */
public static final Comparator RUNDA E IN PREIMAGINE, spre deosebire de commit, si asta e al doilea lucru care nu se sare:
+ * doua PREPARE-uri ale aceluiasi bloc in runde diferite sunt doua afirmatii diferite, iar un
+ * a seal given in one round must not be movable into another. Without the round, a seal from
+ * PREPARE dintr-o runda esuata ar putea fi refolosit ca sa justifice o alta.
+ *
+ * @param chainId the chain id
+ * @param blockNumber the height being prepared
+ * @param round the round number of the prepare
+ * @param digest the block digest the prepare speaks about
+ * @return the 32-byte message to sign
+ */
+ public static Bytes32 prepareMessage(
+ final long chainId, final long blockNumber, final int round, final Bytes digest) {
+ if (blockNumber < 0) {
+ throw new IllegalArgumentException(
+ "AERE PQ PREPARE: blockNumber must not be negative (got " + blockNumber + ")");
+ }
+ if (round < 0) {
+ throw new IllegalArgumentException(
+ "AERE PQ PREPARE: round must not be negative (got " + round + ")");
+ }
+ if (digest == null || digest.size() != 32) {
+ throw new IllegalArgumentException(
+ "AERE PQ PREPARE: digest must be 32 bytes (got "
+ + (digest == null ? "null" : digest.size() + " bytes")
+ + ")");
+ }
+ final BytesValueRLPOutput out = new BytesValueRLPOutput();
+ out.startList();
+ out.writeBytes(PREPARE_DOMAIN_BYTES);
+ out.writeLongScalar(chainId);
+ out.writeLongScalar(blockNumber);
+ out.writeLongScalar(round);
+ out.writeBytes(digest);
+ out.endList();
+ return Hash.keccak256(out.encoded());
+ }
+
/**
* Whether the certificate's validator indices are STRICTLY increasing.
*
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java
index ce9f081..398941c 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -103,19 +103,18 @@ import org.slf4j.LoggerFactory;
* here happens before the node has joined the quorum, is visible in {@code systemctl status} in
* second zero, and is repaired with one line and one restart, at a pace the operator controls. The
* risk that IS real is a bad shared template plus a parallel fleet restart: at quorum 5 of 7 that is
- * not degradation, it is a dead chain. The net for it is the operating rule that goes with this
- * configuration - restart one at a time, never in parallel - and a preflight that computes its
- * verdict from THIS code path rather than from a second reading of the same strings.
+ * not degradation, it is a dead chain. The net for it is the one already written in the runbook -
+ * restart one at a time, never in parallel - and a preflight that computes its verdict from THIS
+ * code path rather than from a second reading of the same strings.
*
- * HONEST LIMITATION, stated in code because it is the same defect class as an unbound
- * registry. The values here are read from LOCAL system properties or environment variables,
- * exactly like {@code aere.falcon.registry} is today. They are NOT yet read from the genesis {@code
- * config.qbft} / {@code config.transitions.qbft}, and there is NO consensus binding on them: two
- * nodes configured with different H or different K schedules will disagree about which headers are
- * valid. Wiring these to genesis, and refusing to start when the Falcon registry does not match the
- * genesis {@code pqRegistryHash}, is a PRECONDITION of arming and is tracked as the fork-activation
- * and registry work items. Until that lands, a non-default value here is a laboratory setting, not
- * a deployment.
+ * HONEST LIMITATION, stated in code because it is the same defect class as A8. The values
+ * here are read from LOCAL system properties or environment variables, exactly like {@code
+ * aere.falcon.registry} is today. They are NOT yet read from the genesis {@code config.qbft} /
+ * {@code config.transitions.qbft}, and there is NO consensus binding on them: two nodes configured
+ * with different H or different K schedules will disagree about which headers are valid. Wiring
+ * these to genesis, and refusing to start when the Falcon registry does not match the genesis {@code
+ * pqRegistryHash}, is a PRECONDITION of arming and is tracked as the fork-activation and registry
+ * work items. Until that lands, a non-default value here is a laboratory setting, not a deployment.
*/
public final class PqAnchorConfig {
@@ -155,10 +154,9 @@ public final class PqAnchorConfig {
* MEASURED 2026-08-07, and this is why the property exists. A Falcon-512 seal is 666 bytes. The
* seal counts observed on a live seven-node run with the threshold at 4 were: 42 blocks with 4, 36
* with 5, 5 with 6. The rehearsal's median header of 3838 bytes is {@code (3838-525)/666 = 4.97}
- * seals. Header bytes therefore scale with the seals actually attached: five seals is about 1.7
- * times what the same chain writes capped at K=3, and about seven times a seal-less header. The
- * figure used before that day had been computed for a SINGLE seal, so it understated the cost by
- * about 4.4x.
+ * seals. Per node per year, at ~165248 blocks/day: one seal 40.2 GB, three 120.5 GB, five 200.9
+ * GB, seven 281.2 GB. The figure carried in our own documents until that day, 45.2 GB/year, is
+ * 1.13 seals: it had been computed for a single seal and was wrong by 4.4x.
*
* Setting this to K therefore removes ~40% of the anchor's disk cost and takes nothing from the
* quorum margin, because the margin is decided by the THRESHOLD a verifier requires, not by how
@@ -190,11 +188,10 @@ public final class PqAnchorConfig {
*
* and it is a knob, not an accident.
*
- * MEASURED 2026-08-07, at K=3 capped, 666 bytes a seal: the certificate cost falls in exact
- * proportion to the interval, so every 10th block costs a tenth of the every-block figure, every
- * 100th a hundredth, every 256th about a 250th. Against a ~523 ms block, an interval of 100 buys
- * that hundredfold saving for a rewritable tail that grows from about half a second to about
- * fifty-two seconds. Algorand ships the same shape at 1 in 256.
+ * MEASURED 2026-08-07, at K=3 capped, ~165248 blocks/day, 666 bytes a seal, per node per year:
+ * every block 120.5 GB; every 10th 12.1 GB; every 100th 1.2 GB; every 256th 0.5 GB. Against a
+ * ~523 ms block, an interval of 100 buys a hundredfold saving for a rewritable tail that grows
+ * from about half a second to about fifty-two seconds. Algorand ships the same shape at 1 in 256.
*
* UNSET MEANS EVERY BLOCK, which is today's design and the strongest setting. As with the seal
* cap, a weakening never arrives as a default; it has to be asked for.
@@ -225,14 +222,14 @@ public final class PqAnchorConfig {
/**
* The stable, greppable code carried by every startup refusal raised while reading this
- * configuration, in the shape of the registry-binding refusal {@code AERE-PQC-REG-MISMATCH-01}.
+ * configuration, in the shape of the A8 registry refusal {@code AERE-PQC-REG-MISMATCH-01}.
*/
public static final String REFUSAL_CODE = "AERE-PQC-ANCHOR-CONF-01";
/**
- * MIN-SEALS FLOOR: the greppable name of the guard that refuses an armed anchor whose schedule
- * never demands a single signature. Named, and not just a message, so that a check can ask whether
- * the guard EXISTS rather than whether some prose happens to be present.
+ * D-147: the greppable name of the guard that refuses an armed anchor whose schedule never demands
+ * a single signature. Named, and not just a message, so that a check can ask whether the guard
+ * EXISTS rather than whether some prose happens to be present.
*/
public static final String REFUSAL_MIN_SEALS_FLOOR = REFUSAL_CODE + "/minSealsFloor";
@@ -561,12 +558,12 @@ public final class PqAnchorConfig {
"AERE PQ ANCHOR: certificate carried every {} block(s) from H={}, not every block. The "
+ "hash chain makes each anchor protect everything BELOW it, so what stays rewritable "
+ "by an adversary holding every classical validator key is the TAIL since the last "
- + "anchor: fork depth <= {} blocks. This is a DELIBERATE weakening bought for header "
- + "size: the certificate cost falls to roughly 1/{} of the every-block figure.",
+ + "anchor: fork depth <= {} blocks. This is a DELIBERATE weakening bought for disk: "
+ + "at K=3 capped it is about {} GB per node per year instead of about 120.",
iv,
config.anchorBlock,
iv,
- iv);
+ String.format("%.1f", 120.5 / iv));
}
if (config.maxSealsCarried().isPresent() && config.everActive()) {
LOG.warn(
@@ -575,7 +572,7 @@ public final class PqAnchorConfig {
+ "header cost and takes NOTHING from the quorum margin, which is decided by the "
+ "threshold a verifier demands, not by how many seals a proposer volunteers above "
+ "it. Measured 2026-08-07: uncapped, a K=3 chain at N=7 carries about five seals, "
- + "which is about 1.7 times the header bytes it writes capped at K.",
+ + "which is 200.9 GB per node per year; capped at K it is 120.5 GB.",
config.maxSealsCarried().getAsInt(),
config.highestEffectiveMinSeals());
}
@@ -731,8 +728,8 @@ public final class PqAnchorConfig {
}
/**
- * THE MIN-SEALS FLOOR. A schedule whose effective K is ZERO at every height leaves the anchor
- * ARMED as a structure and completely toothless, for ever.
+ * D-147, THE MIN-SEALS FLOOR. A schedule whose effective K is ZERO at every height leaves the
+ * anchor ARMED as a structure and completely toothless, for ever.
*
* WHAT THE CODE ALREADY GUARDED, and why that was not enough. The loader already refuses a
* MISSING schedule, in its own words: {@code "the threshold schedule is missing or empty, so K
@@ -740,12 +737,12 @@ public final class PqAnchorConfig {
* exactly what the danger was. But the guard only fired on an ABSENT schedule. A schedule that is
* PRESENT and of the shape {@code WHY THIS IS NOT THEORETICAL: it is the very shape a staged activation recommends, one that
+ * WHY THIS IS NOT THEORETICAL: it is the very shape our activation plan recommends, one that
* STARTS at K=0 as a warm-up window and rises to 3 later. If the second half of the line is lost,
* to a truncated environment variable or a misplaced quote, what is left is {@code The warm-up window stays perfectly legal: this looks at the HIGHEST K in the whole schedule,
* after the emergency ceiling, so {@code H:0,H+165000:3} passes and {@code H:0} on its own does
@@ -1028,8 +1025,9 @@ public final class PqAnchorConfig {
.append(expected)
.append('\n')
.append(" FIX correct BESU_OPTS on THIS node and restart ONLY this node\n")
- .append(" WARNING if the same value is on all seven: restart one at a time,\n")
- .append(" never in parallel. At quorum 5 of 7 you lose the chain.\n")
+ .append(" WARNING if the same value is on every node: restart one at a time,\n")
+ .append(" never in parallel. The chain stops as soon as more than f\n")
+ .append(" validators are down at once, whatever the set size is today.\n")
.append(" EMERGENCY ")
.append(PROPERTY_DISABLE)
.append("=true starts the node with the anchor off and shouts at every block");
@@ -1232,7 +1230,7 @@ public final class PqAnchorConfig {
* and forces A to be reproduced, which needs a Falcon quorum. What an adversary holding every
* classical validator key can still rewrite is the TAIL since the last anchor. Therefore
* {@code fork depth <= interval}. At ~523 ms a block, an interval of 100 is about 52 seconds of
- * rewritable tail, against about half a second at interval 1, at a hundredth of the header cost.
+ * rewritable tail, against about half a second at interval 1, and it costs a hundredth of the disk.
*
* @param interval the interval, or empty for every block
* @return a copy of this configuration carrying the interval
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java
index 7725600..2501920 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -28,8 +28,8 @@ package org.hyperledger.besu.consensus.common.bft;
* WHEN IT CAN HAPPEN. Only above the activation height, and only once the operator has raised K
* past zero: the first stage is required to be K=0, so activation itself can never refuse. In
* steady state the usual cause is a node that restarted and has not yet taken part in a commit, at
- * most one proposer turn. The other cause, f validators withholding Falcon seals, is a measured
- * exposure of its own and is the reason the validator set must grow to N>=9 before K is raised to
+ * most one proposer turn. The other cause, f validators withholding Falcon seals, is the measured
+ * A10 exposure and is the reason the validator set must grow to N>=9 before K is raised to
* quorum, because at N=7, f=2 the margin is exactly zero.
*
* Callers on the consensus path must catch this and simply not propose. It carries the numbers a
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSyncModeGuard.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSyncModeGuard.java
index 73dd049..2dc16d8 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSyncModeGuard.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSyncModeGuard.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -51,8 +51,8 @@ import org.slf4j.LoggerFactory;
* remove.
*
* Inert when the anchor is not configured. With no {@code aere.pq.anchorBlock} this method
- * returns before it looks at the sync mode, so a binary carrying it behaves exactly as it did
- * before on any chain where the anchor is not configured on any node.
+ * returns before it looks at the sync mode, so a binary carrying it behaves exactly as today on
+ * chain 2800 as it stands, where the anchor is not configured on any node.
*/
public final class PqAnchorSyncModeGuard {
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuard.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuard.java
index 6551df0..963c64b 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuard.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuard.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -39,7 +39,7 @@ import org.slf4j.LoggerFactory;
* What is a refusal and what is only a shout. Above {@code quorum - f} the schedule is
- * still reachable but has no margin against f silent or keyless signers, which is a measured
- * exposure of its own. That is a deliberate operator choice with a real cost, so it gets a loud
- * WARN and the node starts. At or above {@code quorum} the schedule is not reachable at all, so it gets a
+ * still reachable but has no margin against f silent or keyless signers, which is the measured A10
+ * and A13 exposure. That is a deliberate operator choice with a real cost, so it gets a loud WARN and
+ * the node starts. At or above {@code quorum} the schedule is not reachable at all, so it gets a
* refusal. A guard that refused both would take the emergency ladder away; a guard that shouted for
* both would be the log line this class exists to replace.
*
@@ -76,8 +76,8 @@ import org.slf4j.LoggerFactory;
* count the validators" is not "the threshold is probably fine".
*
* Inert when the anchor is not armed. With no {@code aere.pq.anchorBlock}, or with the
- * anchor emergency-disarmed, this method returns before it computes anything, so a binary carrying
- * it behaves exactly as it did before on any chain where the anchor is armed on no node.
+ * anchor emergency-disarmed, this method returns before it computes anything, so a binary carrying it
+ * behaves exactly as today on chain 2800, where the anchor is armed on no node.
*/
public final class PqAnchorThresholdGuard {
@@ -91,13 +91,26 @@ public final class PqAnchorThresholdGuard {
/**
* The highest seal threshold that may be configured for a validator set of this size.
*
- * One below the QBFT quorum. At N=7 this is 4.
+ * REVISED 2026-08-20, and the revision is a measurement, not an opinion. Until D-227
+ * (2026-08-14) a proposer could gather at most {@code quorum} seals: the block imported at the
+ * quorum-th Commit and {@code QbftController.consumeMessage} discarded every later Commit, so this
+ * method returned {@code quorum - 1} and the class doc below carries that history. D-227 (the
+ * late-seal salvage, {@code PqLateSealSalvageTest}) extracts the Falcon seal BEFORE the height
+ * gate discards the message, so the cache now accumulates seals from every ALIVE keyed validator.
+ * Measured on mainnet 2800 across 5,400 anchor blocks (2026-08-18..20): certificates carry 8 and 9
+ * seals at N=9, i.e. strictly more than quorum=6, which under the old mechanics was impossible.
+ *
+ * The bound that remains fatal is availability under the tolerated fault budget: with f
+ * validators Byzantine or down, at most {@code N - f} seals can ever exist, so a threshold above
+ * {@code N - f} halts anchors inside the design's own fault model. At N=9 this is 7; at N=7 it is
+ * 5. A threshold at or above the quorum is now a LIVENESS TAX (anchors wait for late seals),
+ * shouted below, not a guaranteed halt.
*
* @param validatorCount the number of validators, at least 1
- * @return the highest configurable threshold K
+ * @return the highest configurable threshold K, {@code N - f}
*/
public static int maxConfigurableThreshold(final int validatorCount) {
- return BftHelpers.calculateRequiredValidatorQuorum(validatorCount) - 1;
+ return validatorCount - byzantineBudget(validatorCount);
}
/**
@@ -150,7 +163,7 @@ public final class PqAnchorThresholdGuard {
}
final int quorum = BftHelpers.calculateRequiredValidatorQuorum(validatorCount);
- final int maxConfigurable = quorum - 1;
+ final int maxConfigurable = maxConfigurableThreshold(validatorCount);
final int f = byzantineBudget(validatorCount);
final int noMarginAbove = quorum - f;
@@ -169,7 +182,7 @@ public final class PqAnchorThresholdGuard {
highest = effective;
highestAt = at;
}
- if (effective >= quorum && fatalHeight < 0L) {
+ if (effective > validatorCount - f && fatalHeight < 0L) {
fatalHeight = at;
fatalThreshold = effective;
}
@@ -187,21 +200,18 @@ public final class PqAnchorThresholdGuard {
+ validatorCount
+ " validators this node is starting into is "
+ quorum
- + " (ceil(2N/3)). The highest threshold that may be configured at this set size is "
+ + " (ceil(2N/3)) with f = "
+ + f
+ + ". The highest threshold that may be configured at this set size is "
+ maxConfigurable
- + ". WHAT THIS MEANS: a proposer builds its certificate out of the Falcon seals it "
- + "heard on Commit messages, and the block is imported at the quorum-th Commit; every "
- + "Commit arriving after that import is discarded as targeting a height not above the "
- + "chain head, so a proposer can gather at most quorum seals at ANY validator-set "
- + "size. Measured on an isolated N=4 network with quorum 3: k=3 on every header above "
- + "the activation height, never 4, with all four nodes keyed and healthy. A threshold "
- + "of "
+ + " = N - f. WHAT THIS MEANS (doctrine revised 2026-08-20 for D-227 late-seal "
+ + "salvage): the seal cache accumulates seals from every ALIVE keyed validator, "
+ + "measured on mainnet 2800 as 8-9 seals per certificate at N=9 across 5,400 anchors. "
+ + "But with f validators Byzantine or down - the design's own fault budget - at most "
+ + "N - f seals can ever exist, so a threshold of "
+ fatalThreshold
- + " therefore requires that ALL of the first "
- + quorum
- + " Commits carry a valid and eligible Falcon seal; one validator without a key among "
- + "them, or one seal that does not verify, and no proposer proposes again. That is a "
- + "halt, not a degradation, and it starts at height "
+ + " makes anchor blocks unreachable inside the tolerated fault model. That is a halt "
+ + "bought by configuration, and it starts at height "
+ fatalHeight
+ ". WHAT TO DO: lower the step to at most "
+ maxConfigurable
@@ -219,6 +229,26 @@ public final class PqAnchorThresholdGuard {
+ ".");
}
+ if (highest >= quorum) {
+ LOG.warn(
+ "AERE PQ ANCHOR: threshold guard PASSED at a QUORUM-OR-ABOVE threshold. K reaches {} at "
+ + "height {}; quorum for {} validators is {} and N - f is {}. Reachability now rests "
+ + "on the D-227 late-seal salvage (measured on mainnet: 8-9 seals per certificate), "
+ + "and the margin under the fault budget is {}: with f={} validators down, anchors "
+ + "wait until {} of the remaining {} carry valid seals. This is the operator's "
+ + "explicit choice of a liveness tax for a quorum-grade certificate.",
+ highest,
+ highestAt,
+ validatorCount,
+ quorum,
+ validatorCount - f,
+ (validatorCount - f) - highest,
+ f,
+ highest,
+ validatorCount - f);
+ return;
+ }
+
if (highest > noMarginAbove) {
LOG.warn(
"AERE PQ ANCHOR: threshold guard PASSED but the schedule has NO MARGIN. K reaches {} at "
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2.java
new file mode 100644
index 0000000..1193143
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2.java
@@ -0,0 +1,185 @@
+/*
+ * AERE crypto-agility, step 2: the versioned anchor certificate.
+ *
+ * WHY A NEW FORMAT. The legacy certificate (PqAnchor.writeCertificate, live on chain 2800) is an
+ * RLP list of [index, signature] pairs: it cannot say WHICH mathematics signed, so it can never
+ * carry the founder-approved hybrid (Falcon + SLH-DSA in one certificate, decision of
+ * 2026-08-07). V2 tags every seal with the one-byte scheme id from SealSchemes.
+ *
+ * HOW THE TWO FORMATS CANNOT BE CONFUSED, by construction and proven in tests:
+ * legacy: RLP [ [idx, sig], ... ] - first element is a LIST
+ * v2: RLP [ 0x02, [ [scheme, idx, sig], ... ] ] - first element is a SCALAR byte
+ * A legacy reader entering v2 bytes finds a scalar where it demands a list and fails loudly; this
+ * decoder REFUSES bytes whose first element is a list (that is legacy, not a malformed v2). The
+ * digest uses a NEW domain string, so a v2 digest can never collide with a v1 digest over related
+ * content: domain separation, same discipline as ANCHOR_DOMAIN v1.
+ *
+ * CANONICAL ORDER. Seals are strictly increasing by (validatorIndex, schemeWireId). One validator
+ * may seal with BOTH schemes (that is the hybrid), but the same (validator, scheme) pair can
+ * appear only once, and any deviation from the canonical order is a decode REFUSAL, not a repair:
+ * a certificate with two encodings would have two digests, and a digest that depends on encoder
+ * mood is not a commitment.
+ */
+package org.hyperledger.besu.consensus.common.bft;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.apache.tuweni.bytes.Bytes32;
+import org.hyperledger.besu.crypto.Hash;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
+import org.hyperledger.besu.ethereum.rlp.RLPInput;
+
+/** Encoder/decoder and digest for the v2 (scheme-tagged) anchor certificate. */
+public final class PqAnchorV2 {
+
+ /** The version scalar that opens every v2 certificate. */
+ public static final int VERSION = 2;
+
+ /** Domain for the v2 anchor digest. NEW string: v1 and v2 digests can never collide. */
+ public static final String ANCHOR_DOMAIN_V2 = "AERE-PQ-ANCHOR-2";
+
+ /** The domain bytes written into every v2 digest preimage. */
+ public static final Bytes ANCHOR_DOMAIN_V2_BYTES =
+ Bytes.wrap(ANCHOR_DOMAIN_V2.getBytes(StandardCharsets.UTF_8));
+
+ /** Canonical order: strictly increasing (validatorIndex, schemeWireId). */
+ public static final Comparator {@link PqRegistryHash} makes the registry a chain-committed object: two nodes cannot hold
* different files without one of them refusing to start. It says so itself, in its own class
* javadoc: "It says nothing about whether any validator actually holds the private key matching its
- * registered public key." That sentence is the whole of the defect this class closes.
+ * registered public key." That sentence is the whole of D-146.
*
* Concretely, the registry is a table from index i to the pair (ECDSA validator address,
* Falcon public key). Seal verification uses the KEY at index i; signer eligibility is checked
@@ -76,7 +75,7 @@ import org.slf4j.LoggerFactory;
* truncated key and that somebody holds the matching secret. It does NOT close T3, T4 or T6:
* at the key ceremony the registry writer holds every Falcon secret, so it can sign a
* possession proof for key 0 sitting under validator 1's address. Anyone who claims a Falcon
- * proof-of-possession repairs this binding defect is wrong, and the probe measures it.
+ * proof-of-possession repairs D-146 is wrong, and the probe measures it.
* THE DEFECT THIS EXISTS TO CLOSE. Measured by reading {@code FalconSealSupport}: the registry
* that answers "which Falcon public key is validator index i" can be loaded from a plain local
@@ -81,7 +81,7 @@ public final class PqRegistryHash {
public static final String DOMAIN_V1 = "AERE-PQ-REGISTRY-1";
/**
- * Domain tag of the canonical v2 pre-image: the same registry PLUS the row binding proofs. A
+ * Domain tag of the canonical v2 pre-image: the same registry PLUS the D-146 binding proofs. A
* separate tag, and not a flag inside v1, so that a v1 file and a v2 file can never hash equal and
* a downgrade that strips the proofs cannot satisfy a schedule entry that was written for v2.
*/
@@ -126,8 +126,8 @@ public final class PqRegistryHash {
private final int index;
private final byte[] address; // 20 bytes, or null when the source is not address-bound
private final byte[] publicKey;
- private final byte[] possessionProof; // row binding proof, or null in a v1 registry
- private final byte[] claimProof; // row binding proof, or null in a v1 registry
+ private final byte[] possessionProof; // D-146, or null in a v1 registry
+ private final byte[] claimProof; // D-146, or null in a v1 registry
Entry(final int index, final byte[] address, final byte[] publicKey) {
this(index, address, publicKey, null, null);
@@ -174,7 +174,7 @@ public final class PqRegistryHash {
}
/**
- * ROW BINDING. The Falcon signature by this row's own key over the binding pre-image, proving somebody
+ * D-146. The Falcon signature by this row's own key over the binding pre-image, proving somebody
* holds the matching secret.
*
* @return the possession proof, or null in a v1 registry
@@ -184,9 +184,8 @@ public final class PqRegistryHash {
}
/**
- * ROW BINDING. The ECDSA signature by this row's own VALIDATOR key over the binding pre-image.
- * This is the half a registry writer cannot forge, and therefore the half that closes both
- * rebinding a row to another validator's address and swapping two rows.
+ * D-146. The ECDSA signature by this row's own VALIDATOR key over the binding pre-image. This is
+ * the half a registry writer cannot forge, and therefore the half that closes T3 and T6.
*
* @return the claim proof, or null in a v1 registry
*/
@@ -209,9 +208,9 @@ public final class PqRegistryHash {
private final String sourcePath;
private final List The shape is deliberately the same as {@code AERE-PQC-REG-ARM-01}, which already refuses to
* arm over a registry with no validator addresses at all, and for the same reason: arming is the
* last moment at which the registry format can still be changed. The anchor contract is immutable
- * once written, so a fleet armed over unbound rows carries that defect for the life of the chain.
+ * once written, so a fleet armed over unbound rows carries D-146 for the life of the chain.
*
* WIRED 2026-08-06. It is called from {@code
* FalconSealSupport.requireRegistryBindingProofsOrAbort()}, in the constructor, immediately after
@@ -484,7 +483,7 @@ public final class PqRegistryHash {
}
throw new RegistryConfigException(
"AERE-PQC-REG-ARM-02",
- "AERE PQC ROW-BINDING: REFUSING TO ARM (fail-closed) over the registry "
+ "AERE PQC D-146: REFUSING TO ARM (fail-closed) over the registry "
+ (registry == null ? "(none)" : "'" + registry.sourcePath() + "'")
+ ", which carries NO binding proofs. Nothing in such a registry connects a Falcon "
+ "public key to the validator address on the same row, so whoever writes the file "
@@ -569,7 +568,7 @@ public final class PqRegistryHash {
} catch (final IOException e) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-01",
- "AERE PQC GENESIS-BINDING: cannot read the Falcon registry file '"
+ "AERE PQC A8: cannot read the Falcon registry file '"
+ path
+ "': "
+ e
@@ -582,7 +581,7 @@ public final class PqRegistryHash {
if (countRaw == null) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-02",
- "AERE PQC GENESIS-BINDING: the Falcon registry '"
+ "AERE PQC A8: the Falcon registry '"
+ path
+ "' has no 'count' property. Refusing to continue (fail-closed): without a declared "
+ "count there is no way to tell a complete registry from one that lost its last "
@@ -591,7 +590,7 @@ public final class PqRegistryHash {
}
final int count = parsePositiveInt(countRaw, "count", path.toString());
- // AERE ROW BINDING. Header fields of the v2 format. A v1 node reading a v2 file does NOT silently
+ // AERE D-146. Header fields of the v2 format. A v1 node reading a v2 file does NOT silently
// ignore these: parseIndexOrThrow refuses an unrecognised key, so an old binary handed a bound
// registry REFUSES rather than loading it with the proofs dropped. That is the correct
// direction of failure and it is why the fields are plain top-level names.
@@ -628,7 +627,7 @@ public final class PqRegistryHash {
if (a.length != 20) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-03",
- "AERE PQC GENESIS-BINDING: registry '"
+ "AERE PQC A8: registry '"
+ path
+ "' entry "
+ i
@@ -679,7 +678,7 @@ public final class PqRegistryHash {
if (cfg.isMissingNode()) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-04",
- "AERE PQC GENESIS-BINDING: genesis '"
+ "AERE PQC A8: genesis '"
+ genesisPath
+ "' has no config.aereFalconRegistry manifest. Refusing to continue (fail-closed).");
}
@@ -700,7 +699,7 @@ public final class PqRegistryHash {
} catch (final IOException e) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-01",
- "AERE PQC GENESIS-BINDING: cannot read '" + path + "': " + e + ". Refusing to continue (fail-closed).");
+ "AERE PQC A8: cannot read '" + path + "': " + e + ". Refusing to continue (fail-closed).");
}
int i = 0;
while (i < raw.length && Character.isWhitespace((char) (raw[i] & 0xff))) {
@@ -721,7 +720,7 @@ public final class PqRegistryHash {
if (cfg == null || cfg.isMissingNode() || !cfg.has("count")) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-05",
- "AERE PQC GENESIS-BINDING: manifest '" + source + "' has no 'count'. Refusing (fail-closed).");
+ "AERE PQC A8: manifest '" + source + "' has no 'count'. Refusing (fail-closed).");
}
final int count = parsePositiveInt(cfg.get("count").asText(), "count", source);
@@ -749,7 +748,7 @@ public final class PqRegistryHash {
} catch (final NumberFormatException e) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-18",
- "AERE PQC GENESIS-BINDING: manifest '"
+ "AERE PQC A8: manifest '"
+ source
+ "' has the unrecognised field '"
+ n
@@ -762,7 +761,7 @@ public final class PqRegistryHash {
if (!strays.isEmpty()) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-09",
- "AERE PQC GENESIS-BINDING: manifest '"
+ "AERE PQC A8: manifest '"
+ source
+ "' declares count="
+ count
@@ -795,7 +794,7 @@ public final class PqRegistryHash {
if (e == null) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-06",
- "AERE PQC GENESIS-BINDING: manifest '"
+ "AERE PQC A8: manifest '"
+ source
+ "' declares count="
+ count
@@ -809,7 +808,7 @@ public final class PqRegistryHash {
if (a == null || !a.isTextual() || k == null || !k.isTextual()) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-07",
- "AERE PQC GENESIS-BINDING: manifest '"
+ "AERE PQC A8: manifest '"
+ source
+ "' entry "
+ i
@@ -819,7 +818,7 @@ public final class PqRegistryHash {
if (addr.length != 20) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-03",
- "AERE PQC GENESIS-BINDING: manifest '"
+ "AERE PQC A8: manifest '"
+ source
+ "' entry "
+ i
@@ -842,7 +841,7 @@ public final class PqRegistryHash {
} else {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-08",
- "AERE PQC GENESIS-BINDING: manifest '"
+ "AERE PQC A8: manifest '"
+ source
+ "' entry "
+ i
@@ -859,7 +858,7 @@ public final class PqRegistryHash {
* -Werror}, so a concrete collection type in a method signature is a build FAILURE, not a style
* note. This file had been type-checked standalone with {@code javac -Xlint:all} and reported
* clean; that is a weaker statement than it sounds, and the difference is the whole reason the
- * wiring had to be compiled in the real tree before this could be called closed. {@code
+ * wiring had to be compiled in the real tree before A8 could be called closed. {@code
* NavigableMap} keeps the guarantee the code actually relies on, which is ascending key order.
*/
private static Registry assemble(
@@ -877,7 +876,7 @@ public final class PqRegistryHash {
if (pks.size() != count) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-09",
- "AERE PQC GENESIS-BINDING: registry '"
+ "AERE PQC A8: registry '"
+ source
+ "' declares count="
+ count
@@ -894,7 +893,7 @@ public final class PqRegistryHash {
if (!pks.containsKey(i)) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-10",
- "AERE PQC GENESIS-BINDING: registry '"
+ "AERE PQC A8: registry '"
+ source
+ "' is missing index "
+ i
@@ -907,7 +906,7 @@ public final class PqRegistryHash {
if (pks.get(i).length == 0) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-11",
- "AERE PQC GENESIS-BINDING: registry '" + source + "' index " + i + " has an EMPTY public key. "
+ "AERE PQC A8: registry '" + source + "' index " + i + " has an EMPTY public key. "
+ "Refusing (fail-closed).");
}
}
@@ -920,7 +919,7 @@ public final class PqRegistryHash {
if (!addrs.containsKey(i)) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-12",
- "AERE PQC GENESIS-BINDING: registry '"
+ "AERE PQC A8: registry '"
+ source
+ "' binds addresses for "
+ addrs.keySet()
@@ -932,7 +931,7 @@ public final class PqRegistryHash {
} else {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-12",
- "AERE PQC GENESIS-BINDING: registry '"
+ "AERE PQC A8: registry '"
+ source
+ "' is MIXED: "
+ addrs.size()
@@ -943,7 +942,7 @@ public final class PqRegistryHash {
}
// ===============================================================================
- // AERE ROW BINDING (2026-08-06). UNIQUENESS. Nothing here needs a signature, and it is the half that
+ // AERE D-146 (2026-08-06). UNIQUENESS. Nothing here needs a signature, and it is the half that
// makes the THRESHOLD real again.
//
// MEASURED on the real verification path: a registry carrying ONE public key at TWO indices was
@@ -965,7 +964,7 @@ public final class PqRegistryHash {
if (first != null) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-19",
- "AERE PQC ROW-BINDING: registry '"
+ "AERE PQC D-146: registry '"
+ source
+ "' carries the SAME Falcon public key at index "
+ first
@@ -987,7 +986,7 @@ public final class PqRegistryHash {
if (first != null) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-20",
- "AERE PQC ROW-BINDING: registry '"
+ "AERE PQC D-146: registry '"
+ source
+ "' binds the SAME validator address 0x"
+ a
@@ -1002,7 +1001,7 @@ public final class PqRegistryHash {
}
}
- // AERE ROW BINDING. FORMAT COHERENCE. Binding proofs are all-or-nothing, exactly like address binding,
+ // AERE D-146. FORMAT COHERENCE. Binding proofs are all-or-nothing, exactly like address binding,
// and for the same reason: one unproven row counts toward the threshold like a proven one.
final boolean anyProof = !pops.isEmpty() || !claims.isEmpty();
final boolean proofBound;
@@ -1010,7 +1009,7 @@ public final class PqRegistryHash {
if (pops.size() != count || claims.size() != count) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-21",
- "AERE PQC ROW-BINDING: registry '"
+ "AERE PQC D-146: registry '"
+ source
+ "' declares count="
+ count
@@ -1025,7 +1024,7 @@ public final class PqRegistryHash {
if (declaredChainId < 0 || bindHeight < 0) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-22",
- "AERE PQC ROW-BINDING: registry '"
+ "AERE PQC D-146: registry '"
+ source
+ "' carries binding proofs but does not declare both 'chainId' and 'bindHeight'. "
+ "Both are inside the signed message, so without them the proofs cannot even be "
@@ -1047,7 +1046,7 @@ public final class PqRegistryHash {
proofBound ? claims.get(i) : null));
if (pks.get(i).length != FALCON_512_PK_LENGTH) {
LOG.warn(
- "AERE PQC GENESIS-BINDING: registry '{}' index {} carries a {}-byte public key; Falcon-512 public "
+ "AERE PQC A8: registry '{}' index {} carries a {}-byte public key; Falcon-512 public "
+ "keys are {} bytes. The registry is NOT rejected for this (the canonical form is "
+ "length-prefixed and handles any length), but it is almost certainly the wrong "
+ "file or a truncated copy.",
@@ -1059,18 +1058,18 @@ public final class PqRegistryHash {
}
final Registry assembled =
new Registry(kind, source, entries, bound, proofBound, declaredChainId, bindHeight);
- // AERE ROW BINDING. THE VERIFICATION ITSELF, on the single path every loader funnels through, so it
+ // AERE D-146. THE VERIFICATION ITSELF, on the single path every loader funnels through, so it
// runs at every restart on every node and not only once at the ceremony. Fail-closed, and O(N)
// per process start with zero cost per block.
PqRegistryBinding.verifyOrThrow(assembled);
if (!proofBound) {
LOG.warn(
- "AERE PQC ROW-BINDING: registry '{}' ({} rows) carries NO binding proofs. Nothing in it "
+ "AERE PQC D-146: registry '{}' ({} rows) carries NO binding proofs. Nothing in it "
+ "connects a Falcon public key to the validator address on the same row, so whoever "
+ "wrote this file decided who every seal is credited to. Measured 2026-08-06 on the "
+ "real verification path: swapping two rows produces an ACCEPTED header with no "
- + "duplicate key and no duplicate address. This is the row-binding defect, and it "
- + "is not closed on this node.",
+ + "duplicate key and no duplicate address. This is D-146 and it is not closed on this "
+ + "node.",
source,
count);
}
@@ -1206,7 +1205,7 @@ public final class PqRegistryHash {
if (node.isEmpty()) {
throw new RegistryConfigException(
"AERE-PQC-REG-SCHED-01",
- "AERE PQC GENESIS-BINDING: "
+ "AERE PQC A8: "
+ source
+ " is an EMPTY array. Refusing to start (fail-closed): an empty schedule is not "
+ "the same statement as an absent one, and guessing which was meant is exactly how "
@@ -1218,7 +1217,7 @@ public final class PqRegistryHash {
if (!e.isObject() || !e.has("block") || !e.has("hash")) {
throw new RegistryConfigException(
"AERE-PQC-REG-SCHED-02",
- "AERE PQC GENESIS-BINDING: "
+ "AERE PQC A8: "
+ source
+ " entry "
+ e
@@ -1228,7 +1227,7 @@ public final class PqRegistryHash {
if (block < 0) {
throw new RegistryConfigException(
"AERE-PQC-REG-SCHED-03",
- "AERE PQC GENESIS-BINDING: "
+ "AERE PQC A8: "
+ source
+ " has a negative or unparseable block in "
+ e
@@ -1237,7 +1236,7 @@ public final class PqRegistryHash {
if (block <= previous) {
throw new RegistryConfigException(
"AERE-PQC-REG-SCHED-04",
- "AERE PQC GENESIS-BINDING: "
+ "AERE PQC A8: "
+ source
+ " blocks are not STRICTLY INCREASING ("
+ previous
@@ -1253,7 +1252,7 @@ public final class PqRegistryHash {
} else {
throw new RegistryConfigException(
"AERE-PQC-REG-SCHED-05",
- "AERE PQC GENESIS-BINDING: "
+ "AERE PQC A8: "
+ source
+ " must be a 0x-prefixed 32-byte hash or an array of {block, hash}; found "
+ node.getNodeType()
@@ -1320,11 +1319,11 @@ public final class PqRegistryHash {
}
// ===================================================================================
- // HEIGHT SCHEDULE: the whole scheduled history, not only the entry in force at the head
+ // D-081: the whole scheduled history, not only the entry in force at the head
// ===================================================================================
/**
- * HEIGHT SCHEDULE. The registries a node holds, indexed by the SCHEDULE ENTRY each one satisfies.
+ * D-081. The registries a node holds, indexed by the SCHEDULE ENTRY each one satisfies.
*
* THE DEFECT THIS EXISTS TO CLOSE, measured and not assumed. {@code config.pqRegistryHash} is a
* schedule, and a second entry is how a key rotation or a revocation is expressed. Enforcement,
@@ -1361,7 +1360,7 @@ public final class PqRegistryHash {
private final List The binding is by HASH and never by order or by file name: a registry covers the entry whose
* required hash it reproduces, and nothing else makes it cover anything. An operator therefore
@@ -1500,8 +1499,8 @@ public final class PqRegistryHash {
if (!hashFor(r, chainId).equalsIgnoreCase(e.hash)) {
continue;
}
- // AERE SIGNED-HEIGHT CHECK (2026-08-06). THE LINK THAT WAS NEVER DRAWN. Both numbers have been in this
- // lexical scope since the height schedule was written and they were never put on the same expression.
+ // AERE D-B (2026-08-06). THE LINK THAT WAS NEVER DRAWN. Both numbers have been in this
+ // lexical scope since D-081 was written and they were never put on the same expression.
//
// bindHeight is the height every row's possession proof and every row's validator claim
// were SIGNED OVER (PqRegistryBinding.bindingPreimage). e.block is the height from which
@@ -1513,9 +1512,9 @@ public final class PqRegistryHash {
// The node and the tool disagreed and nothing put them face to face.
//
// WHAT THAT BUYS AN OPERATOR WHO IS NOT SUPPOSED TO HAVE IT: moving the activation day
- // costs 14 fresh signatures if this is checked, and ZERO if it is not. The seven
- // validators' agreement on a height is only an agreement if something refuses the heights
- // they did not sign.
+ // costs two fresh signatures per validator if this is checked, and ZERO if it is not.
+ // The validators' agreement on a height is only an agreement if something refuses the
+ // heights they did not sign.
if (r.proofBound() && r.bindHeight() != e.block) {
misbound.add(new Misbound(e.block, e.hash, r.sourcePath(), r.bindHeight()));
continue;
@@ -1534,7 +1533,7 @@ public final class PqRegistryHash {
}
/**
- * SIGNED-HEIGHT CHECK. One schedule entry whose required hash a held registry reproduces, and whose height that
+ * D-B. One schedule entry whose required hash a held registry reproduces, and whose height that
* registry's binding proofs were not signed over.
*
* @param entryBlock the height genesis puts the registry in force from
@@ -1546,7 +1545,7 @@ public final class PqRegistryHash {
long entryBlock, String entryHash, String registryPath, long signedHeight) {}
/**
- * HEIGHT SCHEDULE. Load every registry named and bind the result to the schedule.
+ * D-081. Load every registry named and bind the result to the schedule.
*
* @param schedule the parsed schedule
* @param paths the registry files this node holds
@@ -1564,7 +1563,7 @@ public final class PqRegistryHash {
}
/**
- * HEIGHT SCHEDULE. Split a comma-separated list of registry paths. Blank elements are dropped; a null or
+ * D-081. Split a comma-separated list of registry paths. Blank elements are dropped; a null or
* blank list gives an empty result rather than a path named "".
*
* @param raw the configured value, may be null
@@ -1585,7 +1584,7 @@ public final class PqRegistryHash {
}
/**
- * HEIGHT SCHEDULE. The registry in force at a height: the one bound to the schedule entry active there.
+ * D-081. The registry in force at a height: the one bound to the schedule entry active there.
*
* This is the function Falcon verification needs. A certificate in a block at height h was
* produced under the key set the chain required at h, so it must be checked against that key set
@@ -1606,7 +1605,7 @@ public final class PqRegistryHash {
}
/**
- * HEIGHT SCHEDULE. Whether the registries this node holds satisfy the binding active at a height.
+ * D-081. Whether the registries this node holds satisfy the binding active at a height.
*
* The hash is RECOMPUTED here rather than trusted from the binding built earlier, so that this
* answer is a positive proof about the bytes the node is holding right now and not a restatement
@@ -1712,9 +1711,9 @@ public final class PqRegistryHash {
}
/**
- * AERE HELD-SET SCOPE (2026-08-06). THE SAME GUARD, ASKED OF EVERY REGISTRY THIS NODE HOLDS.
+ * AERE D-A (2026-08-06). THE SAME GUARD, ASKED OF EVERY REGISTRY THIS NODE HOLDS.
*
- * WHY THIS OVERLOAD HAD TO EXIST, and it is not tidiness. The height schedule gave a node a HISTORY of
+ * WHY THIS OVERLOAD HAD TO EXIST, and it is not tidiness. D-081 gave a node a HISTORY of
* registry files, one per rotation the chain has ever performed, precisely so that a node can
* validate blocks produced under a retired key set. The startup guard was never told. It compared
* the schedule against ONE registry, the one named by {@code registrySourcePath}, and the history
@@ -1741,19 +1740,19 @@ public final class PqRegistryHash {
final long chainHeadNumber,
final long chainId) {
- // AERE ROW BINDING. The proofs are signed over a chainId the FILE declares. A registry lifted from
+ // AERE D-146. The proofs are signed over a chainId the FILE declares. A registry lifted from
// the scratch chain 442807 carries proofs that verify perfectly among themselves - they are
// internally consistent, just for another chain - and would otherwise pass. Fail-closed here,
// where the node's real chain id is known and the file's is not yet trusted.
//
- // AERE HELD-SET SCOPE (2026-08-06): over EVERY held file, not only the primary. A history file lifted from
+ // AERE D-A (2026-08-06): over EVERY held file, not only the primary. A history file lifted from
// the scratch chain is exactly as dangerous as a primary one - it is the file that answers for
// an interval of history - and before this it was never asked.
for (final Registry r : set.loaded()) {
if (r.proofBound() && r.declaredChainId() != chainId) {
throw new RegistryConfigException(
"AERE-PQC-REG-BIND-07",
- "AERE PQC ROW-BINDING: registry '"
+ "AERE PQC D-146: registry '"
+ r.sourcePath()
+ "' declares chainId="
+ r.declaredChainId()
@@ -1768,7 +1767,7 @@ public final class PqRegistryHash {
if (!schedule.enforced()) {
LOG.warn(
- "AERE PQC GENESIS-BINDING: pqRegistryHash is NOT CONFIGURED ({}), so the Falcon registry is NOT bound "
+ "AERE PQC A8: pqRegistryHash is NOT CONFIGURED ({}), so the Falcon registry is NOT bound "
+ "to consensus on this node. The registry currently loaded is {} ({}, {} entries, "
+ "address-bound={}), canonical hash 0x{}. Two nodes holding DIFFERENT registry files "
+ "will disagree about which public key validator index i has, so the same "
@@ -1779,18 +1778,18 @@ public final class PqRegistryHash {
registry == null ? SourceKind.NONE : registry.kind(),
registry == null ? 0 : registry.count(),
registry != null && registry.addressBound(),
- // GENESIS BINDING, measured on a fleet of seven on 2026-08-06: this printed hashV1 next
- // to the text "put this in config.pqRegistryHash", while THE GATE compares hashFor,
- // which for a registry carrying proofs is hashV2. With the printed value put into
- // genesis, all seven nodes start, all seven report the registry loaded, and THE CHAIN
- // STOPS AT H-1. The guard shouts NOT CORRECTLY STAGED, so it is not a silent halt, but
- // the operator who follows the node's own instruction halts the fleet. A wrong
- // instruction is more dangerous than no instruction at all.
+ // A8, measured on a fleet of seven on 2026-08-06: this printed hashV1 next to the text
+ // "put this in config.pqRegistryHash", while THE GATE compares hashFor, which for a
+ // registry carrying proofs is hashV2. With the printed value put into genesis, all seven
+ // nodes start, all seven report the registry loaded, and THE CHAIN STOPS AT H-1. The
+ // guard shouts NOT CORRECTLY STAGED, so it is not a silent halt, but the operator who
+ // follows the node's own instruction halts the fleet. A wrong instruction is more
+ // dangerous than no instruction at all.
registry == null ? "(no registry)" : hashFor(registry, chainId));
return GateState.NOT_ENFORCED_NO_SCHEDULE;
}
- // AERE SIGNED-HEIGHT CHECK (2026-08-06). THE SILENT DEFERRAL. Placed HERE, below the not-enforced exit above,
+ // AERE D-B (2026-08-06). THE SILENT DEFERRAL. Placed HERE, below the not-enforced exit above,
// and that position is a rule and not a preference: on chain 2800 config.pqRegistryHash does not
// exist, schedule.enforced() is false, and the return above is the first executable statement
// this guard reaches. Nothing new is ever put above it.
@@ -1815,7 +1814,7 @@ public final class PqRegistryHash {
}
throw new RegistryConfigException(
"AERE-PQC-REG-BIND-08",
- "AERE PQC SIGNED-HEIGHT: REFUSING TO START - a registry this node holds is scheduled at a height "
+ "AERE PQC D-B: REFUSING TO START - a registry this node holds is scheduled at a height "
+ "its validators never signed for.\n"
+ " FIELD: 'bindHeight', inside the registry file, versus 'block' of the matching "
+ "entry of config.pqRegistryHash in genesis ("
@@ -1857,8 +1856,8 @@ public final class PqRegistryHash {
// - the operator puts the OLD registry back: the node starts and still cannot pass X.
// Measured.
// Old registry: starts, cannot advance. New registry: cannot start. There was no third file,
- // and the only exit measured was to set the emergency bypass on every node, i.e. to switch
- // the genesis binding off across the whole fleet in order to cross a PLANNED rotation.
+ // and the only exit measured was to set the emergency bypass on every node, i.e. to switch A8
+ // off across the whole fleet in order to cross a PLANNED rotation.
//
// chainHead + 1 is the question the node can actually act on: the only header it will be
// offered next is chainHead + 1, and PqRegistryBindingRule judges that header against the entry
@@ -1873,13 +1872,13 @@ public final class PqRegistryHash {
if (active.isEmpty()) {
final ScheduleEntry first = schedule.entries.get(0);
- // AERE HELD-SET SCOPE: ask the SET, not only the primary. A node staged for the activation may already
+ // AERE D-A: ask the SET, not only the primary. A node staged for the activation may already
// hold the activation registry as history while still signing under the current one.
final Registry staged = set.forEntryBlock(first.block());
final String computed = registry == null ? null : hashFor(registry, chainId);
if (staged != null) {
LOG.info(
- "AERE PQC GENESIS-BINDING: registry binding is scheduled to start at block {} and this node's chain "
+ "AERE PQC A8: registry binding is scheduled to start at block {} and this node's chain "
+ "head is {}, so nothing is enforced yet. The registry already loaded ({}, {} "
+ "entries) ALREADY MATCHES the hash required from block {}: 0x{}. This node is "
+ "correctly staged for the activation.",
@@ -1891,7 +1890,7 @@ public final class PqRegistryHash {
hashFor(staged, chainId));
} else {
LOG.error(
- "AERE PQC GENESIS-BINDING: registry binding starts at block {} and this node's chain head is {}, so "
+ "AERE PQC A8: registry binding starts at block {} and this node's chain head is {}, so "
+ "nothing is enforced yet AND THIS NODE IS NOT CORRECTLY STAGED. Required from "
+ "block {}: 0x{}. Loaded here: {}. This node will run normally and will then "
+ "REFUSE the header at block {} (AERE-PQC-REG-BLOCK-01) and stop there. Install "
@@ -1915,7 +1914,7 @@ public final class PqRegistryHash {
if (set.count() == 0) {
throw new RegistryConfigException(
"AERE-PQC-REG-MISMATCH-02",
- "AERE PQC GENESIS-BINDING: REFUSING TO START.\n"
+ "AERE PQC A8: REFUSING TO START.\n"
+ " EXPECTED: a Falcon validator registry whose canonical hash is\n"
+ " 0x"
+ required.hash()
@@ -1954,14 +1953,14 @@ public final class PqRegistryHash {
}
final String computed = hashFor(registry, chainId);
- // AERE HELD-SET SCOPE (2026-08-06): the binding is satisfied by ANY file this node holds for this entry,
+ // AERE D-A (2026-08-06): the binding is satisfied by ANY file this node holds for this entry,
// not only by the one it signs with. Before this line the answer came from the primary registry
// alone, and after the first rotation the primary is by definition NOT the file that answers for
// the interval below the rotation.
final Registry bound = set.forEntryBlock(required.block());
if (bound != null) {
LOG.info(
- "AERE PQC GENESIS-BINDING: registry binding SATISFIED. Loaded {} ({}, {} entries, address-bound={}); "
+ "AERE PQC A8: registry binding SATISFIED. Loaded {} ({}, {} entries, address-bound={}); "
+ "canonical hash 0x{} equals the hash required from block {} by genesis "
+ "config.pqRegistryHash, read from [{}]. Chain head {}, chainId {}. This node holds "
+ "{} registry file(s) in total. {}",
@@ -1971,7 +1970,7 @@ public final class PqRegistryHash {
bound.addressBound(),
hashFor(bound, chainId),
required.block(),
- // AERE GENESIS BINDING: naming the PROVENANCE of the schedule is not decoration. The whole defect
+ // AERE A8: naming the PROVENANCE of the schedule is not decoration. The whole defect
// class is "a value that came from somewhere nobody checked", so a line that says the
// binding is satisfied without saying what it was read from asserts more than it knows.
schedule.source(),
@@ -2001,7 +2000,7 @@ public final class PqRegistryHash {
throw new RegistryConfigException(
"AERE-PQC-REG-MISMATCH-01",
- "AERE PQC GENESIS-BINDING: REFUSING TO START - the Falcon validator registry on this node is NOT the one "
+ "AERE PQC A8: REFUSING TO START - the Falcon validator registry on this node is NOT the one "
+ "this chain requires.\n"
+ " EXPECTED hash: 0x"
+ required.hash()
@@ -2053,7 +2052,7 @@ public final class PqRegistryHash {
}
/**
- * AERE HELD-SET SCOPE. Every registry file this node holds and which scheduled interval each one answers for.
+ * AERE D-A. Every registry file this node holds and which scheduled interval each one answers for.
* Without this an operator reading {@code MISMATCH-01} cannot tell "I gave this node one file and
* it is the wrong one" from "I gave it four and none covers this height", which are different
* mistakes with different fixes.
@@ -2097,12 +2096,11 @@ public final class PqRegistryHash {
b.append(" entries : ").append(registry.count()).append('\n');
b.append(" addressBound: ").append(registry.addressBound()).append('\n');
b.append(" format: ")
- .append(registry.proofBound() ? "v2, binding proofs present" : "v1, NO binding proofs")
+ .append(registry.proofBound() ? "v2, D-146 binding proofs present" : "v1, NO binding proofs")
.append('\n');
- // GENESIS BINDING: the report printed both v1 and v2 without saying WHICH one goes into
- // genesis, and whoever took the last value off the screen took v1 and halted the fleet at
- // H-1. The one that matters is now named explicitly, and it is the very one the gate
- // compares: hashFor.
+ // A8: the report printed both v1 and v2 without saying WHICH one goes into genesis, and
+ // whoever took the last value off the screen took v1 and halted the fleet at H-1. The one
+ // that matters is now named explicitly, and it is the very one the gate compares: hashFor.
b.append(" >>> FOR config.pqRegistryHash: 0x")
.append(hashFor(registry, chainId))
.append(" <<< this one, and only this one\n");
@@ -2147,7 +2145,7 @@ public final class PqRegistryHash {
} catch (final IOException e) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-13",
- "AERE PQC GENESIS-BINDING: cannot read or parse JSON at '"
+ "AERE PQC A8: cannot read or parse JSON at '"
+ path
+ "': "
+ e
@@ -2165,7 +2163,7 @@ public final class PqRegistryHash {
} catch (final NumberFormatException e) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-14",
- "AERE PQC GENESIS-BINDING: registry '"
+ "AERE PQC A8: registry '"
+ source
+ "' has the unrecognised key '"
+ raw
@@ -2177,7 +2175,7 @@ public final class PqRegistryHash {
}
/**
- * AERE ROW BINDING. Parse an OPTIONAL non-negative header field: absent means -1, present means it must
+ * AERE D-146. Parse an OPTIONAL non-negative header field: absent means -1, present means it must
* be a well formed non-negative number. Absent-or-garbage is never collapsed into a default,
* because a default is how a threshold quietly becomes zero.
*/
@@ -2192,7 +2190,7 @@ public final class PqRegistryHash {
} catch (final NumberFormatException e) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-23",
- "AERE PQC ROW-BINDING: registry '"
+ "AERE PQC D-146: registry '"
+ source
+ "' has "
+ what
@@ -2203,7 +2201,7 @@ public final class PqRegistryHash {
if (v < 0) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-23",
- "AERE PQC ROW-BINDING: registry '"
+ "AERE PQC D-146: registry '"
+ source
+ "' has a negative "
+ what
@@ -2224,7 +2222,7 @@ public final class PqRegistryHash {
} catch (final NumberFormatException e) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-15",
- "AERE PQC GENESIS-BINDING: registry '"
+ "AERE PQC A8: registry '"
+ source
+ "' has a malformed "
+ what
@@ -2249,7 +2247,7 @@ public final class PqRegistryHash {
if (s.isEmpty() || (s.length() & 1) == 1) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-16",
- "AERE PQC GENESIS-BINDING: registry '"
+ "AERE PQC A8: registry '"
+ source
+ "' "
+ what
@@ -2267,7 +2265,7 @@ public final class PqRegistryHash {
if (hi < 0 || lo < 0) {
throw new RegistryConfigException(
"AERE-PQC-REG-LOAD-17",
- "AERE PQC GENESIS-BINDING: registry '"
+ "AERE PQC A8: registry '"
+ source
+ "' "
+ what
@@ -2289,7 +2287,7 @@ public final class PqRegistryHash {
if (s.length() != 64) {
throw new RegistryConfigException(
"AERE-PQC-REG-SCHED-06",
- "AERE PQC GENESIS-BINDING: "
+ "AERE PQC A8: "
+ source
+ " carries the hash '"
+ raw
@@ -2301,7 +2299,7 @@ public final class PqRegistryHash {
if (Character.digit(s.charAt(i), 16) < 0) {
throw new RegistryConfigException(
"AERE-PQC-REG-SCHED-07",
- "AERE PQC GENESIS-BINDING: " + source + " hash '" + raw + "' is not hexadecimal. Refusing to start.");
+ "AERE PQC A8: " + source + " hash '" + raw + "' is not hexadecimal. Refusing to start.");
}
}
return s;
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java
index e5d2675..14071c5 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -23,8 +23,8 @@ import java.util.Map;
import java.util.Optional;
/**
- * REGISTRY SCHEDULE TOOL. The tool that writes and checks the two configuration values without
- * which the height-indexed registry repair changes nothing.
+ * D-145. The tool that writes and checks the two configuration values without which the D2 repair
+ * changes nothing.
*
* WHY IT LIVES IN THE CONSENSUS MODULE AND NOT IN A SCRIPT. The value being computed is a
* keccak digest over a domain-separated, length-prefixed, chain-bound pre-image, and the node
@@ -36,8 +36,8 @@ import java.util.Optional;
*
* THREE VERBS.
@@ -117,13 +117,13 @@ public final class PqRegistryHashTool {
System.out.println("chainId=" + chainId + " (goes INTO the pre-image; 2800 and 442807 give different hashes for the same registry)");
for (final Path p : files) {
final PqRegistryHash.Registry r = PqRegistryHash.loadAuto(p);
- // AERE CANONICAL FINGERPRINT (2026-08-06): hashFor, not hashV1. For a registry that carries
- // binding proofs, hashV1 is a number NOTHING in the node ever compares against: the guard
- // compares hashFor, that is hashV2. The same mistake, in the `generate` verb below, writes
- // into genesis a hash the node will never recognise, and then all seven start and the chain
- // stops at H-1. On top of that, hashV1 does NOT tell two rotation epochs of the same fleet
- // apart, because the bind height does not enter the v1 pre-image; so it cannot serve even as
- // an epoch identifier for diagnostics.
+ // AERE D-C (2026-08-06): hashFor, not hashV1. For a registry that carries binding proofs,
+ // hashV1 is a number NOTHING in the node ever compares against: the guard compares hashFor,
+ // that is hashV2. The same mistake, in the `generate` verb below, writes into genesis a hash
+ // the node will never recognise, and then all seven start and the chain stops at H-1.
+ // On top of that, hashV1 does NOT tell two rotation epochs of the same fleet apart, because the
+ // bind height does not enter the v1 pre-image; so it cannot serve even as an epoch identifier
+ // for diagnostics.
System.out.println(
"0x"
+ PqRegistryHash.hashFor(r, chainId)
@@ -146,8 +146,8 @@ public final class PqRegistryHashTool {
final String armingHeightRaw = o.get("arming-height");
if (armingHeightRaw == null) {
System.out.println("NOT MEASURED: --arming-height AND IT IS NOT THE UNBOUND-REGISTRY DEFECT IN ANOTHER COAT. That one was a registry of public
- * keys read from a file and BELIEVED. Every seal read back here is re-verified, cryptographically,
- * against the anchored registry over M rebuilt from the header this process just loaded - see
- * {@link PqSealStore}. A forged file cannot inject a seal without forging a Falcon-512 signature;
- * the worst it achieves is the empty cache an absent file already gives. Persistence is OFF unless
- * a caller enables it, and the only caller that does is the QBFT controller builder, only when the
- * anchor is actually armed.
+ * AND IT IS NOT DEFECT A8 IN ANOTHER COAT. A8 was a registry of public keys read from a file and
+ * BELIEVED. Every seal read back here is re-verified, cryptographically, against the anchored
+ * registry over M rebuilt from the header this process just loaded - see {@link PqSealStore}. A
+ * forged file cannot inject a seal without forging a Falcon-512 signature; the worst it achieves is
+ * the empty cache an absent file already gives. Persistence is OFF unless a caller enables it, and
+ * the only caller that does is the QBFT controller builder, only when the anchor is actually armed.
*
* WHAT IT DOES NOT DO. The in-memory path verifies nothing. Whether a seal is valid, whether its
* index maps to an eligible validator, and whether there are enough of them, are decided at
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java
index fecb299..e6937dd 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -53,10 +53,10 @@ import org.slf4j.LoggerFactory;
* and nobody could propose. Seals come from Commits, Commits come from proposals, proposals need
* seals - the same circle, one level down.
*
- * WHY THIS IS NOT THE UNBOUND-REGISTRY DEFECT IN ANOTHER COAT, and the distinction is the
- * whole safety argument. That one was a REGISTRY read from a local file and BELIEVED: the
- * public keys that decide who is a legitimate signer came out of a file a node could be pointed at
- * wrongly, so the file was authority. Nothing here is believed. A Falcon seal is SELF-AUTHENTICATING: {@link
+ * WHY THIS IS NOT DEFECT A8 IN ANOTHER COAT, and the distinction is the whole safety
+ * argument. A8 was a REGISTRY read from a local file and BELIEVED: the public keys that decide
+ * who is a legitimate signer came out of a file a node could be pointed at wrongly, so the file was
+ * authority. Nothing here is believed. A Falcon seal is SELF-AUTHENTICATING: {@link
* #readVerified(Path, long, long, Hash, PqSignerRegistry)} re-verifies EVERY seal it reads against
* the anchored registry, over the message M rebuilt from the chain-head header this process just
* loaded, exactly as the producer does at selection time. A forged, edited or replayed file cannot
@@ -320,11 +320,11 @@ public final class PqSealStore {
if (seal.getValidatorIndex() < 0
|| seal.getSignature() == null
|| !seen.add(seal.getValidatorIndex())
- // HEIGHT-RESOLVED LOOKUP (2026-08-06). These seals are over block `blockNumber`, which the
+ // D2 (2026-08-06): height-resolved. These seals are over block `blockNumber`, which the
// caller has already matched against the stored header, so the height is known exactly.
// On a restart at the head this resolves to the same registry it always did; the point is
// that it can no longer resolve to a DIFFERENT one without saying so.
- // OWN-HEAD DOOR (b-v2): the caller has already refused this file unless the
+ // D2 (b-v2): the OWN-HEAD door. The caller has already refused this file unless the
// stored block number and hash equal this node's head, so the subject is this node's
// own head by construction.
|| registry.addressForIndexAtOwnHead(blockNumber, seal.getValidatorIndex()) == null) {
@@ -359,8 +359,7 @@ public final class PqSealStore {
* makes when it decides which heard seals may enter a certificate. Nothing about a seal is trusted
* because it was on disk.
*
- * HEIGHT-RESOLVED LOOKUP (2026-08-06): it now carries the HEIGHT the seals belong to. The
- * adversarial review of
+ * D2 (2026-08-06): it now carries the HEIGHT the seals belong to. The adversarial review of
* 2026-08-02 measured that every registry question in this stack was height-less, so a restart
* after a key rotation re-checked seals over an old block against today's keys and dropped them
* all as forged. Here the height is not in doubt: the caller has already refused the file unless
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java
index 3646b9a..bfaa456 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -37,18 +37,17 @@ import org.apache.tuweni.bytes.Bytes;
public interface PqSignerRegistry {
/**
- * HEIGHT-INDEXED REGISTRY, LOOKUP HARDENING (a). The validator address bound to a registry index
- * AT A HEIGHT.
+ * D-081 / D2 HARDENING (a). The validator address bound to a registry index AT A HEIGHT.
*
* WHY THERE IS NO HEIGHT-LESS FORM HERE, and why this is the change and not a nicety. Until
* 2026-08-06 this interface carried BOTH {@code addressForIndex(int)} and a {@code
* addressForIndexAt(long,int)} whose body was {@code default { return addressForIndex(idx); }}.
- * That default is precisely the defect an adversarial review of 2026-08-02 measured: a registry
+ * That default is precisely what the adversarial review of 2026-08-02 measured as D2: a registry
* has no height argument, so a header that verified yesterday is refused the moment index 0's
- * Falcon key is rotated. The default made the defect INVISIBLE TO ITS OWN PROOF - that review's
- * probe injected a registry that overrides only the height-less pair, inherits the default, and
- * therefore returns exactly the same verdict on repaired and unrepaired code. A probe that cannot
- * go red is not a probe.
+ * Falcon key is rotated. The default made the defect INVISIBLE TO ITS OWN PROOF - the D2 harness
+ * (adversar-2026-08-02/harness/RuleProbe.java lines 115-131) injects a registry that overrides
+ * only the height-less pair, inherits the default, and therefore returns exactly the same verdict
+ * on repaired and unrepaired code. A probe that cannot go red is not a probe.
*
* So the height-less pair is DELETED rather than deprecated, and both survivors are abstract.
* The compiler is now the negative control: any implementation, test double included, that cannot
@@ -56,7 +55,7 @@ public interface PqSignerRegistry {
* on {@link FalconSealSupport} and under a name that cannot be mistaken for a verification path -
* see {@code FalconSealSupport.localSigningAddress()}.
*
- * LOOKUP HARDENING (b-v2), 2026-08-06: this is the HISTORY half of the pair. It is reachable
+ * D2 HARDENING (b-v2), 2026-08-06: this is the HISTORY half of the pair. It is reachable
* only from the two header-validation rules, and it REFUSES an unbound height at or above the
* arming height. The own-head half is {@link #addressForIndexAtOwnHead}, which carries the
* measurement that forced the split.
@@ -69,18 +68,16 @@ public interface PqSignerRegistry {
Address addressForIndexAtHistoric(long blockNumber, int validatorIndex);
/**
- * LOOKUP HARDENING (b-v2). The address bound to a registry index at a height, asked about THIS
- * NODE'S
+ * D2 HARDENING (b-v2). The address bound to a registry index at a height, asked about THIS NODE'S
* OWN HEAD: a block this node is building, or the head it has just restarted onto.
*
* WHY THIS SECOND NAME EXISTS, and it is a measurement and not a taste. The first shape of
* hardening (b) refused every unbound height at or above the arming height and decided that from
* the block NUMBER alone. On 2026-08-06 that turned six tests red - five in {@code
* PqSealPersistenceTest}, the restart path, and one in {@code PqForkValidatorSetChangeTest}, the
- * proposer - and that test's message states the operational consequence in one line: the node
- * stops producing blocks. In all six the number handed to the guard was 1030 with an arming
- * height of 1000, which is exactly what a genuinely historical question at the same instant would
- * hand it.
+ * proposer - and the D078 message states the operational consequence in one line: the node stops
+ * producing blocks. In all six the number handed to the guard was 1030 with an arming height of
+ * 1000, which is exactly what a genuinely historical question at the same instant would hand it.
* There is no arithmetic on the height that separates the two. What separates them is WHO SUPPLIES
* THE SUBJECT, and that is known at every call site and was being thrown away at the boundary.
*
@@ -99,8 +96,7 @@ public interface PqSignerRegistry {
Address addressForIndexAtOwnHead(long blockNumber, int validatorIndex);
/**
- * HEIGHT-INDEXED REGISTRY, LOOKUP HARDENING (a). Verify a Falcon signature by a registry index AT
- * A HEIGHT. Must never
+ * D-081 / D2 HARDENING (a). Verify a Falcon signature by a registry index AT A HEIGHT. Must never
* throw. Abstract for the reason given on {@link #addressForIndexAtHistoric}.
*
* @param blockNumber the height of the header carrying the seal
@@ -112,8 +108,7 @@ public interface PqSignerRegistry {
boolean verifyAtHistoric(long blockNumber, int validatorIndex, Bytes message, Bytes signature);
/**
- * LOOKUP HARDENING (b-v2). Verify a Falcon signature over a block THIS NODE holds as its own head
- * or
+ * D2 HARDENING (b-v2). Verify a Falcon signature over a block THIS NODE holds as its own head or
* is building right now. Never refuses for a missing height binding; see {@link
* #addressForIndexAtOwnHead} for the measurement that forced the split and for what it still does
* refuse.
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SchemeSeal.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SchemeSeal.java
new file mode 100644
index 0000000..ed5a638
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SchemeSeal.java
@@ -0,0 +1,69 @@
+/* AERE crypto-agility, step 2: a seal that names its scheme. The legacy FalconSeal cannot say
+ * what mathematics signed it, so a certificate of FalconSeals can never carry a hybrid. This one
+ * carries the one-byte scheme wire tag from {@link SealSchemes}, which is the whole difference. */
+package org.hyperledger.besu.consensus.common.bft;
+
+import java.util.Objects;
+
+import org.apache.tuweni.bytes.Bytes;
+
+/** One validator seal tagged with the scheme that produced it. Immutable. */
+public final class SchemeSeal {
+
+ private final byte schemeWireId;
+ private final int validatorIndex;
+ private final Bytes signature;
+
+ /** @param schemeWireId the {@link SealScheme#wireId()} of the producing scheme
+ * @param validatorIndex the signer registry index, non-negative
+ * @param signature the raw signature bytes */
+ public SchemeSeal(final byte schemeWireId, final int validatorIndex, final Bytes signature) {
+ this.schemeWireId = schemeWireId;
+ this.validatorIndex = validatorIndex;
+ this.signature = signature;
+ }
+
+ /** The wire tag of the scheme that produced this seal. */
+ public byte getSchemeWireId() {
+ return schemeWireId;
+ }
+
+ /** The signer registry index. */
+ public int getValidatorIndex() {
+ return validatorIndex;
+ }
+
+ /** The raw signature bytes. */
+ public Bytes getSignature() {
+ return signature;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SchemeSeal that)) {
+ return false;
+ }
+ return schemeWireId == that.schemeWireId
+ && validatorIndex == that.validatorIndex
+ && Objects.equals(signature, that.signature);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(schemeWireId, validatorIndex, signature);
+ }
+
+ @Override
+ public String toString() {
+ return "SchemeSeal{scheme=0x"
+ + Integer.toHexString(schemeWireId & 0xff)
+ + ", index="
+ + validatorIndex
+ + ", sig="
+ + signature.size()
+ + "B}";
+ }
+}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java
new file mode 100644
index 0000000..228b9a0
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java
@@ -0,0 +1,96 @@
+/*
+ * AERE crypto-agility layer, step 1 (2026-08-24, TOP-3 list item 9).
+ *
+ * WHY THIS EXISTS. Until today the anchor certificate code talked to exactly one algorithm,
+ * Falcon-512, by name: FalconSeal, FalconSealSupport, FalconPublicKeyParameters. "Safe when the
+ * math changes" was a slogan the code could not honour, because changing the math meant editing
+ * every call site. This interface is the seam that makes the slogan checkable: the protocol talks
+ * to a SealScheme; which lattice (or hash) sits behind it is configuration.
+ *
+ * WHAT IT DELIBERATELY IS NOT. It does not touch FalconSealSupport yet (that rewiring is step 2,
+ * and that file is an overwrite-class file under the D-152 patch discipline). It does not load
+ * private keys from disk (production loading stays per-scheme, exactly as today). It does not
+ * invent a private-key wire encoding: private keys live only as in-memory handles, so no new
+ * secret format exists to leak or to get wrong.
+ */
+package org.hyperledger.besu.consensus.common.bft;
+
+import java.security.SecureRandom;
+import java.util.Optional;
+
+/** A pluggable post-quantum signature scheme for validator seals. Implementations never throw
+ * from {@link #verify}: a malformed key or signature is simply an invalid seal. */
+public interface SealScheme {
+
+ /** Stable human-readable identifier, e.g. {@code "falcon-512"}. Matches the naming the chain
+ * already uses publicly (precompile docs, /v1/pq/verify schemes). */
+ String id();
+
+ /** One-byte wire tag reserved for the versioned certificate format (v2) in which each seal
+ * names its scheme. 0x00 is reserved for "unversioned legacy Falcon". */
+ byte wireId();
+
+ /** Parse the registry form of a public key (the exact bytes a signer registry stores).
+ * Empty when the bytes cannot be a key of this scheme. */
+ Optional DATED NOTE 2026-08-25, refining the sentence at the top of this file. The layer
+ * still does NOT invent a private-key format: the methods below expose exactly the
+ * encoding the scheme's library already has, and only schemes that truly have one
+ * implement them. Measured today on the shipped jar: SLH-DSA-128s has {@code getEncoded()}
+ * with an exact round-trip, so it implements them; Falcon-512 keeps its key in components
+ * and its PRODUCTION loading stays untouched in FalconSealSupport, so it does NOT implement
+ * them and returns empty. Why it was needed: the hybrid producer must be able to receive
+ * the second scheme's key without every call site knowing which scheme it is.
+ *
+ * @param key the private handle
+ * @return the encoding, or empty when this scheme has no canonical one
+ */
+ default Optional This tool calls THE SAME code the startup guard calls. That is the whole point of it existing
* as a class inside {@code consensus:common} rather than as a shell script: a tool that computed the
@@ -94,11 +94,11 @@ public final class PqRegistryHashTool {
return;
}
- // AERE CANONICAL FINGERPRINT (2026-08-06). hashFor, not hashV1, and this is the line an
- // operator pastes into genesis. For a proof-bound (v2) registry the node's gate compares
- // hashV2; printing hashV1 here gives the whole fleet a value NOTHING on a node ever computes.
- // Measured on a network of seven on 2026-08-06: all seven start, all seven report the registry
- // loaded, and the chain stops at H-1. A wrong instruction is more dangerous than a missing one.
+ // AERE D-C (2026-08-06). hashFor, not hashV1, and this is the line an operator pastes into
+ // genesis. For a proof-bound (v2) registry the node's gate compares hashV2; printing hashV1 here
+ // gives the whole fleet a value NOTHING on a node ever computes. Measured on a network of seven
+ // on 2026-08-06: all seven start, all seven report the registry loaded, and the chain stops at
+ // H-1. A wrong instruction is more dangerous than a missing one.
//
// hashV1 is also useless as an epoch identifier, which is the other reason it cannot merely be
// kept alongside: the bind height is not in the v1 pre-image, so two rotation epochs of the same
@@ -110,7 +110,7 @@ public final class PqRegistryHashTool {
return;
}
- System.out.println("AERE PQC GENESIS-BINDING - canonical Falcon registry hash");
+ System.out.println("AERE PQC A8 - canonical Falcon registry hash");
System.out.println(" file : " + reg.sourcePath());
System.out.println(" source kind : " + reg.kind());
System.out.println(" entries : " + reg.count());
@@ -130,7 +130,7 @@ public final class PqRegistryHashTool {
if (reg.proofBound()) {
System.out.println();
System.out.println(
- " AERE HEIGHT BINDING: schedule this registry at block "
+ " AERE D-B: schedule this registry at block "
+ reg.bindHeight()
+ " AND NOWHERE ELSE. Every row's possession proof and validator claim sign that");
System.out.println(
@@ -160,10 +160,10 @@ public final class PqRegistryHashTool {
System.out.println();
System.out.println(" Paste into genesis under \"config\":");
System.out.println();
- // AERE HEIGHT BINDING: the recipe this tool prints has to be the recipe the node accepts.
- // Since 2026-08-06 a proof-bound registry scheduled at a block other than its bindHeight is
- // refused at startup, so printing one here would be manufacturing the configuration the node
- // rejects - in a file that goes to all seven nodes at once.
+ // AERE D-B: the recipe this tool prints has to be the recipe the node accepts. Since 2026-08-06
+ // a proof-bound registry scheduled at a block other than its bindHeight is refused at startup,
+ // so printing one here would be manufacturing the configuration the node rejects - in a file
+ // that goes to all seven nodes at once.
final long at = block == 0L && reg.proofBound() ? reg.bindHeight() : block;
if (reg.proofBound() && at != reg.bindHeight()) {
System.err.println();
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ThresholdReachabilityTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ThresholdReachabilityTest.java
new file mode 100644
index 0000000..8b040bc
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ThresholdReachabilityTest.java
@@ -0,0 +1,354 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.bouncycastle.crypto.digests.KeccakDigest;
+import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
+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;
+
+/**
+ * D-078, THE HALF THAT WAS STILL OPEN: is the threshold K one the fleet can be GUARANTEED to meet?
+ *
+ * The 2026-08-02 repair closed the mechanism that stopped the chain on one add-validator vote: it
+ * took the fleet-wide coverage question out of the per-commit attachment gate and made coverage a
+ * REPORT. That repair is correct and it is measured next door in {@code D078ValidatorSetChangeTest}.
+ * But it left behind an explicit promise, written in the javadoc of {@code attachmentArmed}:
+ *
+ * MEASURED 2026-08-03: {@code armingReadinessDiagnostic()} checks exactly one thing, whether the
+ * manifest is ADDRESS-BOUND. It never reads the fleet size, never reads how many validators hold an
+ * anchored key, and never reads K. The arm-time decision the comment names did not exist, so the
+ * compensating control for the repair was a sentence. This class is what makes it exist.
+ *
+ * THE ARITHMETIC, which is the whole finding and is not an opinion. A block needs {@code
+ * ceil(2N/3)} ECDSA committers, and Falcon seals ride on Commit messages, so the seals a proposer is
+ * GUARANTEED to hear are only those of the keyed validators it cannot avoid: {@code quorum - (N -
+ * keyed)}. The row that matters for this project:
+ *
+ * The second row is the standing plan. "Grow to N=9 BEFORE arming" is right, and if the manifest
+ * is not re-anchored on the way there it produces a fleet that arms a threshold no proposer is
+ * guaranteed to meet. Before this guard a node in that state started, joined, armed, and the failure
+ * appeared later as a proposer that could not propose. That is the most expensive shape a
+ * configuration error can take, and it is the same shape the A8 repair already refused to allow for
+ * a non-address-bound manifest.
+ *
+ * WHY AT CONFIG TIME AND NOWHERE ELSE. The lesson is borrowed, not invented: CometBFT applies a
+ * validator-set change only at H+2 and Ethereum's light-client protocol carries {@code
+ * next_sync_committee} a whole period ahead, both so that the set a cryptographic check runs over is
+ * known and comparable BEFORE the boundary rather than discovered at it. We cannot copy their
+ * mechanism, because at seven nodes under one operator there is no committee to sample. We can copy
+ * the discipline: DECLARE the fleet size, compare it against the threshold at config time, and
+ * refuse to cross the boundary if the comparison fails. The same reasoning already produced
+ * AERE-PQC-CFG-UNSAFE-04 and, for the fork height, AERE-PQC-CFG-UNSAFE-06/07 in D-079.
+ *
+ * NOT MEASURED here, and named so it is not read as covered: what a LIVE fleet does in the rounds
+ * between the vote landing and the first proposer failing. That needs a network. This class measures
+ * the decision, which is the thing a node can be stopped from taking.
+ */
+public class D078ThresholdReachabilityTest {
+
+ /** Anchor activation height H. */
+ private static final long H = 1_000L;
+
+ /** The height from which the staged threshold is K. */
+ private static final long K_AT = H + 10L;
+
+ /** The threshold this project intends to arm. */
+ private static final int K = 5;
+
+ /**
+ * AERE D-146: the chain the registries this fixture writes are BOUND to. It is the same value
+ * {@link #armAnchor} states in {@code aere.pq.chainId}: a registry bound to one chain and an
+ * anchor armed on another is a configuration this fixture must never accidentally describe.
+ */
+ private static final long CHAIN_ID = 2_800L;
+
+ @TempDir private Path tmp;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ resetFalconSingleton();
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ for (final String p :
+ new String[] {
+ "aere.falcon.genesis",
+ "aere.falcon.key",
+ "aere.falcon.attachBlock",
+ "aere.falcon.validatorCount",
+ "aere.falcon.testnetAllowSmallFleet",
+ PqAnchorConfig.PROPERTY_ANCHOR_BLOCK,
+ PqAnchorConfig.PROPERTY_MIN_SEALS,
+ PqAnchorConfig.PROPERTY_CHAIN_ID
+ }) {
+ System.clearProperty(p);
+ }
+ // DATED 2026-08-20, the SECOND time this exact leak was paid for. armAnchor() plus
+ // FalconSealSupport.instance() builds the anchor config through PqAnchorProducer.config(),
+ // whose once-per-JVM cache outlives every property cleared above. Measured today on the
+ // production tree: the armed config this class caches turned all five PqFleetRestartArmingTest
+ // fixtures into AERE-PQC-REG-ARM-02 refusals, green alone, red in the suite, identical sources.
+ // The twin (PqForkThresholdReachabilityTest) has carried this line since 2026-08-11 with the
+ // same story; this class was forked before that fix and never received it.
+ org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.useConfigForTesting(
+ null);
+ resetFalconSingleton();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 1. THE FINDING. A threshold the fleet is not guaranteed to meet must not start.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void armingAThresholdTheFleetCannotGuaranteeMustRefuseToStart() throws Exception {
+ // The exact state the standing plan walks through: the set has grown to nine, the anchored
+ // manifest still names the original seven, and the threshold is the one the schedule arms.
+ writeAnchoredRegistry(7);
+ System.setProperty("aere.falcon.validatorCount", "9");
+ armAnchor(K);
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .describedAs(
+ "N=9 with 7 keyed guarantees only %d Falcon seal(s) among a block's committers, and the "
+ + "armed threshold is K=%d. A node must refuse to start rather than arm a threshold "
+ + "no proposer is guaranteed to be able to meet.",
+ FalconSealSupport.worstCaseKeyedSigners(9, 7), K)
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("AERE-PQC-CFG-UNSAFE-08")
+ // The message has to carry BOTH numbers. "Unsafe" without them sends an operator to read
+ // code; the two numbers are the whole diagnosis and the whole remedy.
+ .hasMessageContaining("K=" + K)
+ .hasMessageContaining("guaranteed");
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 2. NEGATIVE CONTROL. A guard that refuses everything is not a guard.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void aReachableThresholdMustStillStart() throws Exception {
+ // N=7 fully keyed: quorum 5, guaranteed 5, K=5. Margin is exactly zero, which is a different
+ // statement from "unreachable", and the guard must not confuse the two. This is also the
+ // configuration the fleet runs today, so a guard that refused it would be a self-inflicted halt.
+ writeAnchoredRegistry(7);
+ System.setProperty("aere.falcon.validatorCount", "7");
+ armAnchor(K);
+
+ assertThatCode(FalconSealSupport::instance)
+ .describedAs("N=7 fully keyed guarantees exactly K=%d; zero margin is not unreachable", K)
+ .doesNotThrowAnyException();
+ assertThat(FalconSealSupport.instance().registrySize()).isEqualTo(7);
+ }
+
+ @Test
+ public void growingTheManifestWithTheSetIsWhatMakesNineSafe() throws Exception {
+ // The remedy the refusal names, measured rather than asserted: re-anchor the manifest for the
+ // whole set and the same N=9, same K=5 starts.
+ writeAnchoredRegistry(9);
+ System.setProperty("aere.falcon.validatorCount", "9");
+ armAnchor(K);
+
+ assertThatCode(FalconSealSupport::instance).doesNotThrowAnyException();
+ assertThat(FalconSealSupport.worstCaseKeyedSigners(9, 9))
+ .describedAs("nine keyed of nine guarantees the full ECDSA quorum")
+ .isEqualTo(6);
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 3. INERT WHERE IT MUST BE INERT. Chain 2800 as it stands today.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void withNoAnchorConfiguredTheGuardIsInert() throws Exception {
+ // aere.pq.anchorBlock is UNSET on the live chain, so K does not exist and there is nothing to
+ // compare. A guard that could stop a node in that state would be a new way to lose the fleet,
+ // which is a strictly worse defect than the one it repairs.
+ writeAnchoredRegistry(7);
+ System.setProperty("aere.falcon.validatorCount", "9");
+
+ assertThatCode(FalconSealSupport::instance)
+ .describedAs("no anchor configured: no threshold, no comparison, no refusal")
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void aScheduleThatNeverRaisesTheThresholdAboveZeroIsInert() throws Exception {
+ writeAnchoredRegistry(7);
+ System.setProperty("aere.falcon.validatorCount", "9");
+ armAnchor(0);
+
+ assertThatCode(FalconSealSupport::instance)
+ .describedAs("K=0 everywhere is the warm-up regime; nothing can fail to be met")
+ .doesNotThrowAnyException();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 4. THE CASE WITH NO KEYS AT ALL, which is the same arithmetic at its floor.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void aPositiveThresholdWithNoAnchoredKeysMustRefuseToStart() throws Exception {
+ // No manifest anywhere and K=5: guaranteed is 0, so every block at or above H would be rejected
+ // for want of a certificate nobody can produce. Distinct from the A8 refusal, which only fires
+ // when aere.falcon.forkBlock is set; the anchor path has its own arming height.
+ System.setProperty("aere.falcon.validatorCount", "7");
+ armAnchor(K);
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("AERE-PQC-CFG-UNSAFE-08");
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 5. THE WAIVER IS EXPLICIT, NAMED, AND ONLY FOR ISOLATED NETWORKS.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void anIsolatedTestNetworkCanWaiveTheGuardExplicitly() throws Exception {
+ writeAnchoredRegistry(7);
+ System.setProperty("aere.falcon.validatorCount", "9");
+ System.setProperty("aere.falcon.testnetAllowSmallFleet", "true");
+ armAnchor(K);
+
+ assertThatCode(FalconSealSupport::instance)
+ .describedAs(
+ "the same switch that waives the N>=9 rule waives this one, because both say the same "
+ + "thing: this fleet has no Falcon fault margin and must not be a mainnet")
+ .doesNotThrowAnyException();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 6. THE ARITHMETIC ITSELF, at the boundary, as a pure function.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void theDeficitIsTheDistanceBetweenTheThresholdAndTheGuarantee() {
+ assertThat(FalconSealSupport.thresholdDeficit(7, 7, 5))
+ .describedAs("N=7 fully keyed meets K=5 exactly")
+ .isZero();
+ assertThat(FalconSealSupport.thresholdDeficit(8, 7, 5))
+ .describedAs("one unkeyed validator added: still met")
+ .isZero();
+ assertThat(FalconSealSupport.thresholdDeficit(9, 7, 5))
+ .describedAs("two added without re-anchoring: short by one, which is the halt")
+ .isEqualTo(1);
+ assertThat(FalconSealSupport.thresholdDeficit(9, 9, 5)).isZero();
+ assertThat(FalconSealSupport.thresholdDeficit(7, 0, 1))
+ .describedAs("no keys at all: a positive threshold is short by all of it")
+ .isEqualTo(1);
+ assertThat(FalconSealSupport.thresholdDeficit(7, 7, 0))
+ .describedAs("K=0 can never be in deficit")
+ .isZero();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // Helpers.
+ // -------------------------------------------------------------------------------------------
+
+ /** Arm the V2 anchor from system configuration with a staged threshold that reaches {@code k}. */
+ private static void armAnchor(final int k) {
+ System.setProperty(PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, Long.toString(H));
+ // AERE CONFIGURATIE-STRICTA (2026-08-06): an activation height without an explicit
+ // chain id is now a startup refusal, because a silently defaulted 0 in the D and M
+ // pre-images is the Holesky shape. The fixture states what the fleet states.
+ System.setProperty(PqAnchorConfig.PROPERTY_CHAIN_ID, Long.toString(CHAIN_ID));
+ System.setProperty(
+ PqAnchorConfig.PROPERTY_MIN_SEALS, H + ":0," + K_AT + ":" + k);
+ }
+
+ /**
+ * Write a genesis-anchored, address-bound Falcon manifest for {@code count} validators and point
+ * this node at index 0's key, exactly as {@code D078ValidatorSetChangeTest} does. The anchored hash
+ * is accumulated in lockstep with the manifest text, so the fixture is anchored the way a real
+ * genesis is rather than by a flag.
+ */
+ private void writeAnchoredRegistry(final int count) throws Exception {
+ // AERE D-146 (2026-08-06): v2, proof-bound, bound at H, the height armAnchor() arms from. The
+ // rows come from PqV2Fixture because a v2 claim must be signed by the validator whose address
+ // is on the row, and the 0xA00+i addresses this used to spell have no key behind them.
+ final KeccakDigest kd = new KeccakDigest(256);
+ final StringBuilder manifest = new StringBuilder();
+ manifest
+ .append("{\"config\":{\"aereFalconRegistry\":{")
+ .append(PqV2Fixture.manifestHeader(count, CHAIN_ID, H));
+ for (int i = 0; i < count; i++) {
+ final FalconPrivateKeyParameters priv = PqV2Fixture.privateKey(i);
+ final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i);
+ kd.update(anchoredRow, 0, anchoredRow.length);
+ manifest.append(',').append(PqV2Fixture.manifestEntry(i, count, CHAIN_ID, H));
+ if (i == 0) {
+ final Path key0 = tmp.resolve("falcon-key-0.properties");
+ Files.writeString(
+ key0,
+ "index=0\n"
+ + "f="
+ + Bytes.wrap(priv.getSpolyf()).toHexString()
+ + "\n"
+ + "g="
+ + Bytes.wrap(priv.getG()).toHexString()
+ + "\n"
+ + "F="
+ + Bytes.wrap(priv.getSpolyF()).toHexString()
+ + "\n"
+ + "pk="
+ + Bytes.wrap(PqV2Fixture.publicKey(i)).toHexString()
+ + "\n");
+ System.setProperty("aere.falcon.key", key0.toAbsolutePath().toString());
+ }
+ }
+ 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("\"}}}}");
+
+ final Path genesis = tmp.resolve("genesis-registry.json");
+ Files.writeString(genesis, manifest.toString());
+ System.setProperty("aere.falcon.genesis", genesis.toAbsolutePath().toString());
+ }
+
+ private static void resetFalconSingleton() throws Exception {
+ final Field f = FalconSealSupport.class.getDeclaredField("instance");
+ f.setAccessible(true);
+ f.set(null, null);
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ValidatorSetChangeTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ValidatorSetChangeTest.java
new file mode 100644
index 0000000..836c179
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ValidatorSetChangeTest.java
@@ -0,0 +1,428 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
+import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.ethereum.ProtocolContext;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
+
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+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;
+import org.mockito.quality.Strictness;
+
+/**
+ * D-078. THE MEASUREMENT THAT DID NOT EXIST.
+ *
+ * The registry entry reads: "if PQC were armed, an ordinary add-validator vote would stop the
+ * chain: the Falcon blocking quorum follows the dynamic set and cannot be reached inside the vote
+ * window", and it carried {@code verifica: NICIUNA} because "the direct measurement would require
+ * ARMING PQC on a chain, which is exactly the thing that stops the chain".
+ *
+ * That is true of a whole chain. It is NOT true of the decision that stops it. Every step from
+ * "the validator set changed" to "no block can be proposed" is taken by three objects in this
+ * module, each of which is a pure function of its inputs: {@link FalconSealSupport#attachmentArmed}
+ * decides whether this node emits a Falcon seal at all, {@link PqSealCache} holds what was heard,
+ * and {@link PqAnchorProducer#apply} decides whether this node may propose. This class drives those
+ * three with a REAL address-bound genesis-anchored registry and REAL Falcon-512 keys, and asks the
+ * question the registry says cannot be asked.
+ *
+ * WHAT EACH TEST MEASURES, and why each of them can fail:
+ *
+ * NOT MEASURED here, deliberately, and named so it is not mistaken for covered: how many rounds a
+ * live fleet takes to stop once every proposer refuses, and what a syncing node does meanwhile.
+ * Those need a network, and the network run is separate evidence.
+ */
+public class D078ValidatorSetChangeTest {
+
+ /** Anchor activation height H used throughout. */
+ private static final long H = 1_000L;
+
+ /** Seal-attachment height, comfortably below H. */
+ private static final long ATTACH = 900L;
+
+ /** Height from which the staged threshold K is 5, i.e. the armed regime. */
+ private static final long K_AT = H + 10L;
+
+ private static final int K = 5;
+
+ private static final int N = 7;
+
+ private static final long CHAIN_ID = 220_878L;
+
+ @TempDir private Path tmp;
+
+ private final List The three rows below are the ones that decide the project's own arming order, so they are
+ * measured here rather than reasoned about in a document:
+ *
+ * Read against the standing rule "grow to N=9 BEFORE arming", that third row is the warning:
+ * growing to nine while the anchored manifest still names seven is exactly the state in which a
+ * proposer can legitimately fail to assemble a certificate. The manifest has to grow with the set.
+ */
+ @Test
+ public void theCostOfAnUnkeyedValidatorIsANumberAndTheNumberIsThis() {
+ assertThat(FalconSealSupport.worstCaseKeyedSigners(7, 7))
+ .describedAs("N=7 fully keyed: K=5 is met with zero margin")
+ .isEqualTo(5);
+ assertThat(FalconSealSupport.worstCaseKeyedSigners(8, 7))
+ .describedAs("one validator added without re-anchoring: K=5 still met, still zero margin")
+ .isEqualTo(5);
+ assertThat(FalconSealSupport.worstCaseKeyedSigners(9, 7))
+ .describedAs(
+ "two added without re-anchoring: below K=5, so a proposer can legitimately fail. This "
+ + "is the row that constrains growing to N=9 before arming.")
+ .isEqualTo(4);
+ assertThat(FalconSealSupport.worstCaseKeyedSigners(7, 0)).isZero();
+ assertThat(FalconSealSupport.worstCaseKeyedSigners(0, 0)).isZero();
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // 5. NEGATIVE CONTROL for this whole file: the gate must still refuse what it must refuse.
+ // -----------------------------------------------------------------------------------------
+
+ /**
+ * Every other test here asserts that the gate says YES. Replace {@code attachmentArmed} with
+ * {@code return true} and all of them still pass, which would make this file a proof that cannot
+ * fail. These four assertions are what makes that substitution impossible: each names a condition
+ * the D-078 repair deliberately did NOT touch.
+ *
+ * @throws Exception if the fixture cannot be rebuilt
+ */
+ @Test
+ public void theGateStillRefusesEverythingItMustStillRefuse() throws Exception {
+ // (1) below the configured attachment height.
+ assertThat(FalconSealSupport.instance().attachmentArmed(ATTACH - 1L))
+ .describedAs("below the attachment height nothing may be attached")
+ .isFalse();
+
+ // (2) no attachment height configured at all, which is the default and the state of chain 2800.
+ System.clearProperty("aere.falcon.attachBlock");
+ resetFalconSingleton();
+ assertThat(FalconSealSupport.instance().attachmentArmed(H + 5L))
+ .describedAs("with aere.falcon.attachBlock unset a node holding a key attaches nothing")
+ .isFalse();
+
+ // (3) attachment height reached, but no anchored registry to be checked against.
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.clearProperty("aere.falcon.genesis");
+ resetFalconSingleton();
+ assertThat(FalconSealSupport.instance().attachmentArmed(H + 5L))
+ .describedAs("a seal is never emitted against a registry that cannot be checked")
+ .isFalse();
+
+ // (4) anchored, address-bound registry, but it does not bind THIS node's index. The seal would
+ // be unattributable, so the seals rule would refuse the whole header carrying it.
+ System.setProperty("aere.falcon.genesis", genesisPath.toAbsolutePath().toString());
+ final Path strayKey = tmp.resolve("falcon-key-stray.properties");
+ Files.writeString(strayKey, Files.readString(key0Path).replace("index=0", "index=42"));
+ System.setProperty("aere.falcon.key", strayKey.toAbsolutePath().toString());
+ resetFalconSingleton();
+ final FalconSealSupport stray = FalconSealSupport.instance();
+ assertThat(stray.genesisAnchored())
+ .describedAs("the registry must still load, or (4) would pass for the wrong reason")
+ .isTrue();
+ assertThat(stray.attachmentArmed(H + 5L))
+ .describedAs("an index the anchored registry does not bind must not attach")
+ .isFalse();
+ assertThat(stray.sign(H + 5L, message(H + 4L))).isEmpty();
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // Helpers.
+ // -----------------------------------------------------------------------------------------
+
+ private static Bytes32 message(final long blockNumber) {
+ return PqAnchor.commitMessage(CHAIN_ID, blockNumber, Bytes32.leftPad(Bytes.of(1)));
+ }
+
+ private static byte[] falconSign(final FalconPrivateKeyParameters key, final Bytes32 m) {
+ final FalconSigner signer = new FalconSigner();
+ signer.init(true, key);
+ return signer.generateSignature(m.toArray());
+ }
+
+ private static ProtocolContext contextWith(final Collection The registry entry reads: "a malformed forkBlock falls OPEN, with only a log line, and arming
+ * it at or before the anchor observation height passes undetected", and it carried {@code verifica:
+ * NICIUNA} since 18 July. This file is the command that can fail.
+ *
+ * Both halves of the finding are about the SAME shape of defect, the one the Holesky Pectra
+ * incident of February 2025 made expensive for everybody: a fork-activation parameter that is wrong
+ * or absent does not stop the node, it changes what the node silently believes. Half one is the
+ * value itself. Half two is the ORDER between that value and the height at which the registry the
+ * value depends on becomes active.
+ *
+ * WHAT EACH TEST MEASURES, and how each can fail:
+ *
+ * NOT MEASURED here, deliberately, and named so it is not mistaken for covered: whether a real
+ * Besu node process exits with a non-zero status when this exception is thrown. This class measures
+ * the decision, not the process. The exception is thrown from the constructor, on the same path as
+ * the guards that already abort, and nothing in this tree catches {@code
+ * FalconSealSupport.ActivationConfigException}.
+ */
+public class D079ForkArmingTest {
+
+ /** Fleet size; nine, because the blocking guard refuses to arm below nine. */
+ private static final int N = 9;
+
+ /** Height at which the on-chain late-anchor registry contract is expected to be observed. */
+ private static final long OBSERVE = 5_000L;
+
+ /** Seal-attachment height: at or after OBSERVE, so a seal can actually be emitted. */
+ private static final long ATTACH = 6_000L;
+
+ /** Blocking height: at least minAttachLead (256) after ATTACH. */
+ private static final long FORK = 7_000L;
+
+ private static final String ANCHOR_ADDRESS = "0x0000000000000000000000000000000000000fa1";
+
+ /**
+ * AERE D-146: the chain this fixture's registries are BOUND to. Every proof commits to it, so it
+ * has to be stated rather than defaulted.
+ */
+ private static final long CHAIN_ID = 2_800L;
+
+ @TempDir private Path tmp;
+
+ private Path manifestPath;
+ private Path genesisPath;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ // AERE D-146 (2026-08-06): both registries below are v2 and PROOF-BOUND, bound at FORK, the
+ // height this fixture arms from. They used to carry addresses spelled 0xB00+i, which no
+ // secp256k1 key can sign for, so this whole fixture became unstartable the moment
+ // AERE-PQC-REG-ARM-02 was wired into the constructor.
+
+ // LATE-ANCHOR manifest: the registry is PENDING until the anchor contract is observed on chain.
+ final StringBuilder late = new StringBuilder("{");
+ late.append(PqV2Fixture.manifestHeader(N, CHAIN_ID, FORK));
+ for (int i = 0; i < N; i++) {
+ late.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, FORK));
+ }
+ late.append("}");
+ manifestPath = tmp.resolve("falcon-late-manifest.json");
+ Files.writeString(manifestPath, late.toString());
+
+ // GENESIS-ANCHORED manifest: the registry is ACTIVE from block 0, so no observation height can
+ // exist and none may be demanded. Built exactly the way a real genesis is, hash included.
+ final KeccakDigest kd = new KeccakDigest(256);
+ final StringBuilder gen = new StringBuilder("{\"config\":{\"aereFalconRegistry\":{");
+ gen.append(PqV2Fixture.manifestHeader(N, CHAIN_ID, FORK));
+ for (int i = 0; i < N; i++) {
+ final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i);
+ kd.update(anchoredRow, 0, anchoredRow.length);
+ gen.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, FORK));
+ }
+ final byte[] anchoredHash = new byte[32];
+ kd.doFinal(anchoredHash, 0);
+ gen.append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x")
+ .append("0".repeat(64))
+ .append("\":\"0x")
+ .append(Bytes.wrap(anchoredHash).toUnprefixedHexString())
+ .append("\"}}}}");
+ genesisPath = tmp.resolve("genesis-registry.json");
+ Files.writeString(genesisPath, gen.toString());
+
+ System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
+ resetFalconSingleton();
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ for (final String p :
+ new String[] {
+ "aere.falcon.manifest",
+ "aere.falcon.genesis",
+ "aere.falcon.anchor.address",
+ "aere.falcon.anchor.block",
+ "aere.falcon.attachBlock",
+ "aere.falcon.forkBlock",
+ "aere.falcon.validatorCount"
+ }) {
+ System.clearProperty(p);
+ }
+ resetFalconSingleton();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 1. The fixture's own control.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void controlAWellFormedLateAnchorConfigurationStarts() {
+ lateAnchor();
+ System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE));
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
+
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+ assertThat(pqc.lateAnchorPending())
+ .describedAs(
+ "the late-anchor manifest must load and stay PENDING, or every refusal below is a "
+ + "refusal about a registry that was never there")
+ .isTrue();
+ assertThat(pqc.forkBlock()).isEqualTo(FORK);
+ assertThat(pqc.attachBlock()).isEqualTo(ATTACH);
+ assertThat(pqc.forkBlock())
+ .describedAs(
+ "the ordering the guard exists to enforce, stated as a property: blocking arms strictly "
+ + "AFTER the height at which the registry it depends on can become active")
+ .isGreaterThan(OBSERVE);
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 2-3. Half one at config time.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void aMalformedForkBlockRefusesToStart() {
+ lateAnchor();
+ System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE));
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ // The exact typo shape a human makes when copying a height out of a document.
+ System.setProperty("aere.falcon.forkBlock", "9_189_161");
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .describedAs(
+ "a malformed blocking height must ABORT, never degrade to never-blocking with a log line")
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("MALFORMED");
+ }
+
+ @Test
+ public void aNegativeForkBlockRefusesToStart() {
+ lateAnchor();
+ System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE));
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.forkBlock", "-1");
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("negative");
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 4. Half one where it actually survived: the value was validated but never OWNED.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void theForkBlockIsResolvedOnceAndCannotBeReopenedAfterStartup() {
+ lateAnchor();
+ System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE));
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
+
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+ assertThat(pqc.forkBlock()).isEqualTo(FORK);
+
+ // The startup guard has already run and passed. Nothing will run it again. If the accessor
+ // re-reads the property, then the ONE decision the whole PQC layer is gated on is a value that
+ // can still turn into "never blocking" at any moment, for any reason that leaves the property
+ // unparseable, and the only trace is one WARN line per call.
+ System.setProperty("aere.falcon.forkBlock", "not-a-number");
+ assertThat(pqc.forkBlock())
+ .describedAs(
+ "the blocking height must be resolved ONCE, at the boundary, and owned thereafter. A "
+ + "value that is validated at startup and re-parsed on every use is not validated.")
+ .isEqualTo(FORK);
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 5-7. Half two: the ORDER between the blocking height and the anchor observation height.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void blockingOverAPendingAnchorWithNoDeclaredObservationHeightRefuses() {
+ lateAnchor();
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
+ // aere.falcon.anchor.block deliberately NOT set.
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .describedAs(
+ "with the registry still PENDING and no stated activation height, nothing in this "
+ + "process can compare the blocking height against the height at which the registry "
+ + "becomes usable, so the ordering error the finding names cannot be detected at all")
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("AERE-PQC-CFG-UNSAFE-06");
+ }
+
+ @Test
+ public void anAttachHeightBeforeTheObservationHeightRefuses() {
+ lateAnchor();
+ System.setProperty("aere.falcon.anchor.block", Long.toString(ATTACH + 1L));
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .describedAs(
+ "attachment before the registry can be active emits nothing, so the log-only soak "
+ + "window measures nothing and the blocking height arrives over a registry no node "
+ + "has ever produced a seal against")
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("AERE-PQC-CFG-UNSAFE-07");
+ }
+
+ @Test
+ public void aForkHeightAtTheObservationHeightRefuses() {
+ lateAnchor();
+ System.setProperty("aere.falcon.anchor.block", Long.toString(FORK));
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .describedAs("the literal stimulus in the finding: armed AT the anchor observation height")
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("AERE-PQC-CFG-UNSAFE-07");
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 8-9. The new value must fail closed like every other one.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void aMalformedObservationHeightRefuses() {
+ lateAnchor();
+ System.setProperty("aere.falcon.anchor.block", "1e3");
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("AERE-PQC-CFG-SYNTAX-09");
+ }
+
+ @Test
+ public void aNegativeObservationHeightRefuses() {
+ lateAnchor();
+ System.setProperty("aere.falcon.anchor.block", "-5");
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("AERE-PQC-CFG-SYNTAX-10");
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 10-11. Scope controls. A guard that refuses everything is not a guard.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void aGenesisAnchoredRegistryNeedsNoObservationHeight() {
+ System.setProperty("aere.falcon.genesis", genesisPath.toAbsolutePath().toString());
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
+ // aere.falcon.anchor.block deliberately NOT set: a genesis-anchored registry is active from
+ // block 0, so there IS no observation height and demanding one would break the whole
+ // genesis-anchored deployment path.
+
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+ assertThat(pqc.genesisAnchored()).isTrue();
+ assertThat(pqc.addressBound()).isTrue();
+ assertThat(pqc.forkBlock()).isEqualTo(FORK);
+ }
+
+ @Test
+ public void anObservationHeightWithoutBlockingIsHarmless() {
+ lateAnchor();
+ System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE));
+ // No forkBlock, no attachBlock: the log-only baseline every node on chain 2800 runs today.
+
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+ assertThat(pqc.forkBlock()).isEqualTo(Long.MAX_VALUE);
+ assertThat(pqc.attachBlock()).isEqualTo(Long.MAX_VALUE);
+ assertThat(pqc.lateAnchorPending()).isTrue();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // Helpers.
+ // -------------------------------------------------------------------------------------------
+
+ private void lateAnchor() {
+ System.setProperty("aere.falcon.manifest", manifestPath.toAbsolutePath().toString());
+ System.setProperty("aere.falcon.anchor.address", ANCHOR_ADDRESS);
+ }
+
+ private static void resetFalconSingleton() throws Exception {
+ final Field f = FalconSealSupport.class.getDeclaredField("instance");
+ f.setAccessible(true);
+ f.set(null, null);
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D081RegistryRotationTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D081RegistryRotationTest.java
new file mode 100644
index 0000000..12c091e
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D081RegistryRotationTest.java
@@ -0,0 +1,484 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * D-081: the Falcon signer registry has no usable rotation and no usable revocation.
+ *
+ * WHAT IS MEASURED HERE, and why it is measured against the real classes rather than described.
+ * {@code config.pqRegistryHash} is a SCHEDULE of {block, hash} entries, and the A8 dossier states
+ * that "a later entry expresses a key rotation". This file asks whether that sentence survives
+ * contact with the code that enforces it.
+ *
+ * The enforcement side is {@link PqRegistryHash#matchesAt} and, on the block path, {@code
+ * FalconSealSupport.registryBindingSatisfiedAt(height)}, which calls it. Both take exactly ONE
+ * loaded registry, and the node loads exactly one file ({@code aere.falcon.registry}). The entry the
+ * schedule makes active at a height decides which hash is required THERE. So after one rotation at
+ * H2 there are two intervals with two different required hashes, and one file can satisfy at most
+ * one of them.
+ *
+ * The consequence is not cosmetic and it is not confined to the rotation moment. {@code
+ * PqRegistryBindingRule} is a DETACHED rule, so it runs on the header-download path, and {@code
+ * PqAnchorSyncModeGuard} refuses to start an armed node in anything but FULL sync. A node acquiring
+ * history therefore validates every height, including the interval before the rotation. Holding the
+ * post-rotation registry it is refused there; holding the pre-rotation registry it is refused at the
+ * head. There is no third choice. ONE rotation makes the chain permanently unjoinable.
+ *
+ * This is the lesson Cosmos ADR-016 writes down explicitly: a rotation scheme has to keep the
+ * MAPPING FROM HEIGHT TO KEY SET, not only the current key set, or blocks signed under the old set
+ * stop being verifiable. Cosmos may bound that history by the unbonding period. We may not: chain
+ * 2800 has no unbonding period and a node syncing from genesis must verify every block that was ever
+ * produced, so every entry ever scheduled has to stay loadable forever.
+ *
+ * {@code rotationDoesNotBrickHistory} and {@code revocationDoesNotBrickHistory} are the
+ * measurement. They FAIL while the defect is present and pass only when a node can be configured to
+ * satisfy the binding at EVERY scheduled height at once. The other tests are controls: they assert
+ * that the schedule really does express rotation and really does refuse a malformed one, so a
+ * failure of the two measurements cannot be blamed on the fixture.
+ */
+public class D081RegistryRotationTest {
+
+ private static final long CHAIN_ID = 2800L;
+
+ /** First binding height: the height the post-quantum registry is first enforced from. */
+ private static final long H1 = 12_000_000L;
+
+ /** Rotation height: from here the chain requires the SECOND registry. */
+ private static final long H2 = 12_100_000L;
+
+ /** Falcon-512 public key length as this registry format stores it (bare h polynomial). */
+ private static final int PK_LENGTH = 896;
+
+ /** The seven validators of chain 2800. */
+ private static final int N = 7;
+
+ /**
+ * One node configuration, expressed as the only question the consensus path ever asks it: does
+ * the registry material this node holds satisfy the binding the chain requires at this height?
+ *
+ * It is an interface and not a Registry so that the measurement can be stated once and asked of
+ * every configuration a node can actually be put into. Today there is exactly one shape of answer,
+ * {@link #single}, because a node loads one file. A repair that lets a node hold the whole
+ * scheduled history adds a second shape here and the assertion below stops failing. Nothing in the
+ * assertion has to change, which is the point: the property is fixed, the capability is what moves.
+ */
+ private interface NodeConfiguration {
+ boolean satisfiesAt(long height);
+
+ String describe();
+ }
+
+ private static NodeConfiguration single(
+ final String name, final PqRegistryHash.Schedule schedule, final PqRegistryHash.Registry r) {
+ return new NodeConfiguration() {
+ @Override
+ public boolean satisfiesAt(final long height) {
+ return PqRegistryHash.matchesAt(schedule, r, height, CHAIN_ID);
+ }
+
+ @Override
+ public String describe() {
+ return "node holding only registry " + name;
+ }
+ };
+ }
+
+ // ---------------------------------------------------------------------------------------
+ // Fixture. Two registries that differ in exactly one row, which is what both a rotation and a
+ // revocation look like on the wire: index 3 stops being the key it was.
+ // ---------------------------------------------------------------------------------------
+
+ private static byte[] deterministicKey(final int index, final int generation) {
+ final byte[] pk = new byte[PK_LENGTH];
+ for (int i = 0; i < pk.length; i++) {
+ pk[i] = (byte) ((i * 31) + (index * 7) + (generation * 101));
+ }
+ return pk;
+ }
+
+ private static byte[] address(final int index) {
+ final byte[] a = new byte[20];
+ for (int i = 0; i < a.length; i++) {
+ a[i] = (byte) ((index * 17) + i);
+ }
+ return a;
+ }
+
+ private static String hex(final byte[] b) {
+ final StringBuilder sb = new StringBuilder(b.length * 2);
+ for (final byte x : b) {
+ sb.append(String.format("%02x", x));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * A seven-row address-bound registry. {@code rotatedIndex} is the row whose key belongs to
+ * generation 2; every other row is generation 1. Passing -1 gives the untouched registry.
+ */
+ private static Path writeRegistry(final Path dir, final String name, final int rotatedIndex)
+ throws IOException {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("count=").append(N).append('\n');
+ for (int i = 0; i < N; i++) {
+ sb.append(i).append('=').append(hex(deterministicKey(i, i == rotatedIndex ? 2 : 1))).append('\n');
+ sb.append(i).append(".addr=").append(hex(address(i))).append('\n');
+ }
+ final Path p = dir.resolve(name);
+ Files.write(p, sb.toString().getBytes(StandardCharsets.UTF_8));
+ return p;
+ }
+
+ private static PqRegistryHash.Schedule scheduleOf(final String hashAtH1, final String hashAtH2) {
+ final String json =
+ "[{\"block\":"
+ + H1
+ + ",\"hash\":\"0x"
+ + hashAtH1
+ + "\"},{\"block\":"
+ + H2
+ + ",\"hash\":\"0x"
+ + hashAtH2
+ + "\"}]";
+ final JsonNode node;
+ try {
+ node = new ObjectMapper().readTree(json);
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
+ return PqRegistryHash.parseSchedule(node, "D-081 fixture");
+ }
+
+ /** Every height at which the binding is enforced and could differ across the rotation. */
+ private static List MEASURED FIRST, ON A NETWORK, NOT ASSUMED. The full activation rehearsal on a seven-node test
+ * network (repetitie-activare-2026-08-05) found that with the anchor armed at K>0 a SIMULTANEOUS
+ * restart of every validator stops the chain for good. The node said it verbatim: "refusing to
+ * propose ... holds 0 valid eligible Falcon seal(s) ... threshold is 3", over "Attachment stays OFF
+ * (fail-safe)".
+ *
+ * THE CIRCLE. {@code activateLateAnchor()} used to be reachable from exactly one place, {@code
+ * FalconSealValidationRule.tryActivateLateAnchor}, which runs only while a block is being IMPORTED.
+ * After a fleet restart no block is imported, because nobody proposes. So {@code lateActivated}
+ * stays false, {@link FalconSealSupport#attachmentArmed(long)} answers false, no seal is attached,
+ * no certificate reaches K, and nobody can propose. Seals come from Commits, Commits come from
+ * proposals, proposals need seals. With K=0 the chain heals itself. With K>0 it never does.
+ *
+ * WHAT THIS CLASS MEASURES, and it is the state machine the repair moves, not a paraphrase of
+ * it. The repair (QbftBesuControllerBuilder, marker "AERE BLOCAJ-REPORNIRE") adds a SECOND caller of
+ * the SAME method at startup, reading the SAME contract slot 0 out of the chain-head world state.
+ * So the question that decides whether the repair can work is exactly: does calling {@code
+ * activateLateAnchor} with the on-chain hash, with no block imported and no other stimulus, turn
+ * {@code attachmentArmed()} from false to true. Below, it does.
+ *
+ * NOT MEASURED here, and named so it is not read as covered: that a real Besu process reads slot
+ * 0 out of a real chain-head world state (that is world-state plumbing in the app module, and the
+ * rehearsal network is the instrument for it), and that seven live nodes recover from a real
+ * simultaneous restart with this binary. This class measures the decision the deadlock hinges on.
+ */
+public class D140FleetRestartArmingTest {
+
+ /**
+ * Fleet size for THIS fixture. Not a statement about any live network: the 2026-08-05 decision
+ * to stay at seven was reversed, and the set has been nine since 2026-08-12. Seven is kept here
+ * because it is the size at which the margin arithmetic this class exercises is tightest.
+ */
+ private static final int N = 7;
+
+ /** Height at which the anchor contract is expected to be observable. */
+ private static final long OBSERVE = 1_000L;
+
+ /** Seal-attachment height, at or after OBSERVE. */
+ private static final long ATTACH = 1_200L;
+
+ /** A chain head well past the attachment height: this is what a restart comes back to. */
+ private static final long HEAD = 5_000L;
+
+ private static final String ANCHOR_ADDRESS = "0x0000000000000000000000000000000000000fa1";
+
+ @TempDir private Path tmp;
+
+ /** keccak256 over (addr20 || pk) for every index in order: what the anchor contract holds. */
+ private String onChainHash;
+
+ /** The same registry, spelled as a GENESIS-anchored manifest (the rehearsal's own shape). */
+ private Path genesisPath;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ resetFalconSingleton();
+
+ final SecureRandom rnd = SecureRandomProvider.createSecureRandom();
+ final KeccakDigest kd = new KeccakDigest(256);
+ final StringBuilder manifest = new StringBuilder("{\"count\":").append(N);
+ final StringBuilder genesis =
+ new StringBuilder("{\"config\":{\"aereFalconRegistry\":{\"count\":").append(N);
+ for (int i = 0; i < N; i++) {
+ final FalconKeyPairGenerator gen = new FalconKeyPairGenerator();
+ gen.init(new FalconKeyGenerationParameters(rnd, FalconParameters.falcon_512));
+ final AsymmetricCipherKeyPair kp = gen.generateKeyPair();
+ final FalconPublicKeyParameters pub = (FalconPublicKeyParameters) kp.getPublic();
+ final FalconPrivateKeyParameters priv = (FalconPrivateKeyParameters) kp.getPrivate();
+ final Address addr = Address.fromHexString(String.format("0x%040x", 0xA00 + i));
+
+ // The pre-image is accumulated in lockstep with the manifest text, exactly the way a real
+ // anchoring transaction is built, so the hash below is not copied out of the code under test.
+ final byte[] addrBytes = addr.getBytes().toArray();
+ kd.update(addrBytes, 0, addrBytes.length);
+ kd.update(pub.getH(), 0, pub.getH().length);
+
+ final String entry =
+ ",\""
+ + i
+ + "\":{\"addr\":\""
+ + addr.toHexString()
+ + "\",\"pk\":\""
+ + Bytes.wrap(pub.getH()).toHexString()
+ + "\"}";
+ manifest.append(entry);
+ genesis.append(entry);
+
+ if (i == 0) {
+ // This node is validator 0 and HOLDS a signing key, otherwise attachment is off for a
+ // reason that has nothing to do with the deadlock and the measurement would be vacuous.
+ final Path key0 = tmp.resolve("falcon-key-0.properties");
+ Files.writeString(
+ key0,
+ "index=0\n"
+ + "f="
+ + Bytes.wrap(priv.getSpolyf()).toHexString()
+ + "\n"
+ + "g="
+ + Bytes.wrap(priv.getG()).toHexString()
+ + "\n"
+ + "F="
+ + Bytes.wrap(priv.getSpolyF()).toHexString()
+ + "\n"
+ + "pk="
+ + Bytes.wrap(pub.getH()).toHexString()
+ + "\n");
+ System.setProperty("aere.falcon.key", key0.toAbsolutePath().toString());
+ }
+ }
+ manifest.append("}");
+
+ final byte[] digest = new byte[32];
+ kd.doFinal(digest, 0);
+ onChainHash = Bytes.wrap(digest).toUnprefixedHexString();
+
+ final Path manifestPath = tmp.resolve("falcon-late-manifest.json");
+ Files.writeString(manifestPath, manifest.toString());
+
+ // Same seven entries, anchored the way the rehearsal network anchored them: in genesis, with
+ // the hash committed in the anchor contract's slot 0 through alloc storage.
+ genesis
+ .append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x")
+ .append("0".repeat(64))
+ .append("\":\"0x")
+ .append(onChainHash)
+ .append("\"}}}}");
+ genesisPath = tmp.resolve("genesis-registry.json");
+ Files.writeString(genesisPath, genesis.toString());
+
+ System.setProperty("aere.falcon.manifest", manifestPath.toAbsolutePath().toString());
+ System.setProperty("aere.falcon.anchor.address", ANCHOR_ADDRESS);
+ System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE));
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ for (final String p :
+ new String[] {
+ "aere.falcon.manifest",
+ "aere.falcon.genesis",
+ "aere.falcon.key",
+ "aere.falcon.anchor.address",
+ "aere.falcon.anchor.block",
+ "aere.falcon.attachBlock",
+ "aere.falcon.forkBlock",
+ "aere.falcon.validatorCount"
+ }) {
+ System.clearProperty(p);
+ }
+ resetFalconSingleton();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 1. The deadlock state, stated as a property.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void restartedFleetIsNotArmedAndDoesNotHealWithTime() {
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+
+ assertThat(pqc.lateAnchorPending())
+ .describedAs(
+ "fixture control: the late-anchor manifest must LOAD and stay PENDING, or every "
+ + "assertion below is about a registry that was never configured")
+ .isTrue();
+ assertThat(pqc.lateAnchored()).isFalse();
+ assertThat(pqc.lateAnchorFailed()).isFalse();
+ assertThat(pqc.signingEnabled())
+ .describedAs("fixture control: this node holds a Falcon key, so attachment is not off for "
+ + "the trivial reason")
+ .isTrue();
+ assertThat(pqc.attachBlock()).isEqualTo(ATTACH);
+
+ // This IS the post-restart state: the process has just started, the chain head is far past the
+ // attachment height, and no block has been imported because nobody has proposed one.
+ assertThat(pqc.attachmentArmed(HEAD))
+ .describedAs(
+ "the measured deadlock: attachment height long since passed, registry still pending, "
+ + "so no seal is attached and no certificate can ever reach K")
+ .isFalse();
+
+ // And it does not heal. Time, and blocks that are never imported, change nothing.
+ for (long n = HEAD; n <= HEAD + 10_000L; n += 1_000L) {
+ assertThat(pqc.attachmentArmed(n))
+ .describedAs("still not armed at height %s; nothing in the process flips it", n)
+ .isFalse();
+ }
+ assertThat(pqc.registrySize())
+ .describedAs("the registry is EMPTY while pending, which is why a seal cannot verify either")
+ .isZero();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 2. The repair's mechanism: the SECOND caller, the one startup adds.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void activatingFromTheChainHeadArmsAttachment() {
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+ assertThat(pqc.attachmentArmed(HEAD)).isFalse();
+
+ // Exactly what the startup repair does: hand over the 32-byte value read from the anchor
+ // contract's slot 0 in the CHAIN-HEAD world state. No block is imported anywhere here.
+ final boolean activated = pqc.activateLateAnchor(onChainHash);
+
+ assertThat(activated).isTrue();
+ assertThat(pqc.lateAnchored()).isTrue();
+ assertThat(pqc.lateAnchorPending()).isFalse();
+ assertThat(pqc.registrySize()).isEqualTo(N);
+ assertThat(pqc.addressBound())
+ .describedAs("the activated registry must bind every index to an address, or a seal cannot "
+ + "be resolved to a signer")
+ .isTrue();
+ assertThat(pqc.attachmentArmed(HEAD))
+ .describedAs(
+ "THE REPAIR: one activation from chain-head state arms attachment, so a restarted "
+ + "validator emits Falcon-carrying Commits again, certificates reach K, and a "
+ + "proposer can propose. This is the edge the deadlock needed and did not have.")
+ .isTrue();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 3. THE NEGATIVE CONTROL. The repair must not have bought liveness by weakening the check.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void aWrongOnChainHashLeavesAttachmentOffAndIsTerminal() {
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+
+ // One flipped nibble: a tampered anchor, or a wrong manifest shipped to this node.
+ final char first = onChainHash.charAt(0);
+ final String wrong = (first == '0' ? '1' : '0') + onChainHash.substring(1);
+ assertThat(wrong).isNotEqualTo(onChainHash).hasSize(64);
+
+ assertThat(pqc.activateLateAnchor(wrong))
+ .describedAs("a mismatching anchor must NOT activate the registry")
+ .isFalse();
+ assertThat(pqc.lateAnchored()).isFalse();
+ assertThat(pqc.lateAnchorFailed())
+ .describedAs("and the mismatch must be TERMINAL, not merely 'not yet'")
+ .isTrue();
+ assertThat(pqc.registrySize())
+ .describedAs("the registry stays EMPTY: fail-closed, not fail-open")
+ .isZero();
+ assertThat(pqc.attachmentArmed(HEAD))
+ .describedAs(
+ "attachment stays OFF after a failed activation. If this were true, the startup repair "
+ + "would have turned a tamper detection into an arming path.")
+ .isFalse();
+
+ // And the correct hash afterwards does not resurrect it: a node that has seen a tampered anchor
+ // stays refused, which is the same fail-closed rule the import path already had.
+ assertThat(pqc.activateLateAnchor(onChainHash)).isFalse();
+ assertThat(pqc.attachmentArmed(HEAD)).isFalse();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 4. Scope control: two callers now exist in one process.
+ // -------------------------------------------------------------------------------------------
+
+ @Test
+ public void activationIsIdempotentAcrossRepeatedStartupCalls() {
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+
+ assertThat(pqc.activateLateAnchor(onChainHash)).isTrue();
+ final int afterFirst = pqc.registrySize();
+
+ // The startup call has fired; the import path fires too, on the first block that arrives.
+ assertThat(pqc.activateLateAnchor(onChainHash)).isTrue();
+ assertThat(pqc.registrySize()).isEqualTo(afterFirst).isEqualTo(N);
+ assertThat(pqc.attachmentArmed(HEAD)).isTrue();
+
+ // Even a garbage hash after activation cannot un-arm it: activation is a one-way latch, so a
+ // second reader with a stale view cannot disarm a fleet that is already sealing.
+ assertThat(pqc.activateLateAnchor("00".repeat(32))).isTrue();
+ assertThat(pqc.lateAnchorFailed()).isFalse();
+ assertThat(pqc.attachmentArmed(HEAD)).isTrue();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 5. The rehearsal's OWN stimulus, replayed against THIS tree. Read the note before trusting it.
+ // -------------------------------------------------------------------------------------------
+
+ /**
+ * The seven-node rehearsal ran a GENESIS-anchored registry, and the line it logged after the
+ * simultaneous restart was the COVERAGE one: "no validator set has been observed yet, so registry
+ * COVERAGE cannot be proven. Attachment stays OFF (fail-safe)". That condition does not exist in
+ * this tree: {@code grep} for it returns nothing, because D-078 (2026-08-02) removed the fleet
+ * question from the per-commit gate. The rehearsal binary was built from the 2026-08-01 tree,
+ * which still had it.
+ *
+ * So this test states what is true HERE: a genesis-anchored node, freshly constructed, with no
+ * validator set observed and no block imported, IS armed. The rehearsal's measured deadlock is
+ * closed for the genesis-anchored path by a repair that already landed - and NOT by the startup
+ * repair this class is about.
+ *
+ * Which is exactly why the startup repair is still needed: on the LATE-ANCHOR path, the one
+ * the live chain must use because it cannot be re-genesised, {@code lateActivated} is still set
+ * from one place only. Tests 1-3 measure that path.
+ *
+ * NOT MEASURED: that seven live nodes on a genesis-anchored network recover from a
+ * simultaneous restart with a binary built from this tree.
+ */
+ @Test
+ public void aGenesisAnchoredNodeIsArmedImmediatelyAfterRestart() throws Exception {
+ System.clearProperty("aere.falcon.manifest");
+ System.clearProperty("aere.falcon.anchor.address");
+ System.clearProperty("aere.falcon.anchor.block");
+ System.setProperty("aere.falcon.genesis", genesisPath.toAbsolutePath().toString());
+ resetFalconSingleton();
+
+ final FalconSealSupport pqc = FalconSealSupport.instance();
+
+ assertThat(pqc.genesisAnchored())
+ .describedAs("fixture control: the genesis manifest must verify against the anchored hash")
+ .isTrue();
+ assertThat(pqc.registrySize()).isEqualTo(N);
+ assertThat(pqc.addressBound()).isTrue();
+ assertThat(pqc.attachmentArmed(HEAD))
+ .describedAs(
+ "a genesis-anchored node arms with NO validator set observed and NO block imported. "
+ + "The rehearsal's coverage condition is gone from this tree.")
+ .isTrue();
+ }
+
+ private static void resetFalconSingleton() throws Exception {
+ final Field f = FalconSealSupport.class.getDeclaredField("instance");
+ f.setAccessible(true);
+ f.set(null, null);
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D141SealPersistenceTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D141SealPersistenceTest.java
new file mode 100644
index 0000000..d66a209
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D141SealPersistenceTest.java
@@ -0,0 +1,621 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
+import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
+import org.hyperledger.besu.crypto.SecureRandomProvider;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.ethereum.ProtocolContext;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
+
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+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;
+import org.mockito.quality.Strictness;
+
+/**
+ * D-141. THE SECOND HALF OF THE FLEET-RESTART CHAIN DEATH: the heard seals themselves.
+ *
+ * MEASURED ON A NETWORK FIRST, NOT ASSUMED. With the anchor armed at K>0, a SIMULTANEOUS
+ * restart of all seven validators kills the chain permanently (rehearsal
+ * repetitie-activare-2026-08-05, isolated chain 330858). The FIRST half of that deadlock was the
+ * registry, repaired the same day: it now activates at start-up from chain-head state, and all seven
+ * nodes reported "activare a ancorei la PORNIRE din starea capului: REUSITA". The chain died anyway.
+ * The refusal only changed shape, from "registry address-bound=false" to "registry
+ * address-bound=TRUE ... Heard 0 seal(s)", frozen 150 s then 298 s.
+ *
+ * THE SECOND CIRCLE. The Falcon seals over M(head) travel on nothing but the Commit messages of
+ * the head block, and those are never replayed after a restart. They exist nowhere else: the head's
+ * own header carries a certificate over its PARENT, not over itself. So every node came back holding
+ * zero seals, none could reach K, none could propose, and therefore none ever sent another Commit
+ * for another node to hear. Seals come from Commits, Commits come from proposals, proposals need
+ * seals.
+ *
+ * WHAT THIS CLASS MEASURES, one test per link, with the causal chain driven in BOTH directions so
+ * that "refuses" is never satisfied by a producer that always refuses:
+ *
+ * NOT MEASURED here, and named so it is not read as covered: that seven live nodes recover from a
+ * real simultaneous restart with a binary built from this tree. That needs the rehearsal network and
+ * is separate evidence. This class measures every decision that recovery depends on.
+ */
+public class D141SealPersistenceTest {
+
+ /** Anchor activation height H. */
+ private static final long H = 1_000L;
+
+ /** Seal-attachment height, comfortably below H. */
+ private static final long ATTACH = 900L;
+
+ /** Height from which the staged threshold K is in force. */
+ private static final long K_AT = H + 10L;
+
+ /** The founder's decision of 2026-08-05: N=7 stays, and K=3 is the value with full margin. */
+ private static final int K = 3;
+
+ private static final int N = 7;
+
+ private static final long CHAIN_ID = 2_800L;
+
+ /** Measured block interval on the live chain, in milliseconds. */
+ private static final long BLOCK_INTERVAL_MS = 523L;
+
+ @TempDir private Path tmp;
+
+ /** Stands in for the node's data directory, which is where the real path comes from. */
+ private Path dataDirectory;
+
+ private final List Three shapes of forgery are in the one file, because "a forged seal" is not one thing:
+ *
+ * WHAT THE WIRE BUYS, stated as the thing that is actually true. Before it, an ARMED node loaded
+ * a v1 registry without a word, and the registry decides who a Falcon seal is credited to. Measured
+ * on the real verification path on the same day: two rows with their public keys swapped - four
+ * distinct keys, four distinct addresses, so no uniqueness check would see anything - produced an
+ * ACCEPTED header; and the same key filed at two indices satisfied a threshold of two with one
+ * private key, which makes the threshold itself fiction.
+ *
+ * WHY THE POSITIVE CONTROLS ARE THE EXPENSIVE HALF. A gate that refuses everything is not a gate,
+ * it is an outage wearing a security message. The tests that cost the most to get right here are the
+ * ones where the node STARTS: over a correct v2 registry, and over the very same v1 file when
+ * nothing is armed.
+ *
+ * WHY THE ANCHOR CASE IS TESTED SEPARATELY FROM THE FORK-BLOCK CASE. They are different triggers
+ * and only one of them was previously guarded at all. {@code armingReadinessDiagnostic()} returns
+ * immediately when {@code aere.falcon.forkBlock} is unset, so AERE-PQC-REG-ARM-01 has never fired on
+ * a node armed through the certificate anchor. This guard fires on both, and {@link
+ * #armedThroughTheANCHORAloneTheNodeAlsoREFUSES} is the half that has no predecessor.
+ *
+ * WHAT IS NOT MEASURED HERE, written rather than implied: nothing is deployed, no node is
+ * started, the fleet of seven is not touched, and every Falcon and ECDSA key below is a PROBE key
+ * generated in this JVM. Whether the refusal behaves the same on the seven real boxes at a
+ * coordinated restart is NOT MEASURED.
+ */
+public class D146ArmingGateTest {
+
+ /** The height at which this fixture arms Falcon blocking. */
+ private static final long FORK = 7_000L;
+
+ /** Attachment must lead the fork block; the same shape D079ForkArmingTest uses. */
+ private static final long ATTACH = 6_000L;
+
+ /** The chain id the registry is bound to. Not 2800: nothing here may look like the live fleet. */
+ private static final long CHAIN_ID = 220_878L;
+
+ /** The height the binding proofs are signed for. */
+ private static final long BIND_HEIGHT = FORK;
+
+ private static final int N = 4;
+
+ /** Every property this class is allowed to touch. Cleared before AND after every test. */
+ private static final List This is the case with no predecessor. AERE-PQC-REG-ARM-01 is raised by {@code
+ * armingReadinessDiagnostic()}, whose first statement is to return when the fork block is unset,
+ * so an anchor-armed node has never been asked ANY question about its registry's shape at startup.
+ */
+ @Test
+ public void armedThroughTheANCHORAloneTheNodeAlsoREFUSES() throws Exception {
+ final Path v1 = writeRegistry("registru-v1.properties", false, false);
+ System.setProperty("aere.falcon.registry", v1.toAbsolutePath().toString());
+ armWithAnchorOnly();
+
+ assertThat(System.getProperty("aere.falcon.forkBlock"))
+ .describedAs("this test is only worth something while the fork block is genuinely unset")
+ .isNull();
+ assertThatThrownBy(FalconSealSupport::instance)
+ .isInstanceOf(PqRegistryHash.RegistryConfigException.class)
+ .hasMessageContaining("AERE-PQC-REG-ARM-02");
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 2. THE POSITIVE CONTROLS. Without these the refusals above could be a load bug.
+ // -------------------------------------------------------------------------------------------
+
+ /**
+ * The same node, the same arming, over a registry whose every row carries a Falcon possession
+ * proof and an ECDSA claim signed by that row's own validator key, STARTS - and loads.
+ */
+ @Test
+ public void armedOverAV2RegistryTheNodeSTARTS() throws Exception {
+ armWithForkBlock(writeRegistry("registru-v2.properties", true, true));
+
+ assertThatCode(FalconSealSupport::instance)
+ .describedAs(
+ "POSITIVE CONTROL: the gate can be green. A refusal that no correct input can pass is "
+ + "an outage wearing a security message")
+ .doesNotThrowAnyException();
+ assertThat(FalconSealSupport.instance().registrySize())
+ .describedAs("and it must really have loaded the file, not merely declined to throw")
+ .isEqualTo(N);
+ }
+
+ /** The same, armed through the anchor alone. */
+ @Test
+ public void armedThroughTheANCHORAloneOverAV2RegistryTheNodeSTARTS() throws Exception {
+ final Path v2 = writeRegistry("registru-v2.properties", true, true);
+ System.setProperty("aere.falcon.registry", v2.toAbsolutePath().toString());
+ armWithAnchorOnly();
+
+ assertThatCode(FalconSealSupport::instance).doesNotThrowAnyException();
+ assertThat(FalconSealSupport.instance().registrySize()).isEqualTo(N);
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 3. THE BOUNDARY. A node that arms NOTHING must be untouched by any of this.
+ // -------------------------------------------------------------------------------------------
+
+ /**
+ * THE GUARANTEE FOR CHAIN 2800 AS IT STANDS: a node with no {@code aere.pq.*} property and no
+ * {@code aere.falcon.forkBlock} starts over the very same v1 file that is refused when armed.
+ *
+ * The assertion that carries the weight is not the "starts" - it is the property sweep. A test
+ * that only asserted "does not throw" would keep passing if a later edit made the guard read some
+ * other property that happened to be set in this JVM. The sweep states the precondition as a
+ * measurement: at the moment the constructor runs, NO system property beginning with {@code
+ * aere.pq.} exists, and neither does the fork block.
+ */
+ @Test
+ public void withNothingArmedTheGateIsInertOverTheSameV1Registry() throws Exception {
+ final Path v1 = writeRegistry("registru-v1.properties", false, false);
+ System.setProperty("aere.falcon.registry", v1.toAbsolutePath().toString());
+
+ assertThat(systemPropertiesStartingWith("aere.pq."))
+ .describedAs("the precondition of this test, measured rather than assumed")
+ .isEmpty();
+ assertThat(System.getProperty("aere.falcon.forkBlock")).isNull();
+
+ assertThatCode(FalconSealSupport::instance)
+ .describedAs(
+ "the same file that is refused when armed is accepted when nothing is armed, so the "
+ + "trigger is ARMING and not the file")
+ .doesNotThrowAnyException();
+ assertThat(FalconSealSupport.instance().registrySize())
+ .describedAs("and an unarmed node's registry is loaded exactly as it was before D-146")
+ .isEqualTo(N);
+ }
+
+ /**
+ * The same boundary with NO registry configured either, which is a node holding nothing at all -
+ * the shape of a fresh box joining the fleet before any key ceremony.
+ */
+ @Test
+ public void aNodeWithNoFalconConfigurationAtAllStarts() {
+ assertThat(systemPropertiesStartingWith("aere.pq.")).isEmpty();
+ assertThat(System.getProperty("aere.falcon.registry")).isNull();
+
+ assertThatCode(FalconSealSupport::instance).doesNotThrowAnyException();
+ }
+
+ /**
+ * An ARMED node with no registry file at all is deliberately NOT this guard's business, and this
+ * test is what stops that from being a silent decision.
+ *
+ * D-146 is mis-ATTRIBUTION, which needs rows; an empty registry credits nobody. The condition
+ * is owned by AERE-PQC-CFG-UNSAFE-08 when the threshold is positive, and MEASURED here: with a
+ * threshold of zero, which is the warm-up regime the fleet is meant to arm INTO, the node starts.
+ * An earlier revision of this guard refused here, and the cost was exactly that - the intended
+ * activation procedure became unstartable.
+ */
+ @Test
+ public void armedWithNoRegistryAtAllAndAZeroThresholdTheNodeStarts() {
+ System.setProperty(PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, Long.toString(FORK));
+ System.setProperty(PqAnchorConfig.PROPERTY_CHAIN_ID, Long.toString(CHAIN_ID));
+ System.setProperty(PqAnchorConfig.PROPERTY_MIN_SEALS, FORK + ":0");
+ System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
+ System.setProperty("aere.falcon.testnetAllowSmallFleet", "true");
+
+ assertThatCode(FalconSealSupport::instance)
+ .describedAs("K=0 over an empty registry is the warm-up regime, not a D-146 defect")
+ .doesNotThrowAnyException();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // 4. HALF A v2 REGISTRY IS NOT A v2 REGISTRY.
+ // -------------------------------------------------------------------------------------------
+
+ /**
+ * A row that carries a Falcon possession proof and no ECDSA claim proves that SOMEBODY holds the
+ * key, and says nothing about which validator asked for it - which is the whole of D-146.
+ *
+ * MEASURED, and the assertion was CHANGED to match the measurement rather than the other way
+ * round. The expectation written first was AERE-PQC-REG-ARM-02. What actually happens is a refusal
+ * one step EARLIER, at load, with AERE-PQC-REG-LOAD-21, because the loader counts proofs against
+ * claims and refuses a half-bound file before the arming gate ever sees it. That is the stronger
+ * of the two refusals - it holds whether or not the node is armed - so this is asserted on the
+ * code that actually fires.
+ */
+ @Test
+ public void possessionWithoutAClaimIsRefusedEarlierStillAtLoad() throws Exception {
+ armWithForkBlock(writeRegistry("registru-doar-posesie.properties", true, false));
+
+ assertThatThrownBy(FalconSealSupport::instance)
+ .describedAs(
+ "the attacker is the key holder, so a genuine possession proof over a lying row is "
+ + "genuinely produceable; only the validator's own signature closes it")
+ .isInstanceOf(PqRegistryHash.RegistryConfigException.class)
+ .hasMessageContaining("AERE-PQC-REG-LOAD-21");
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // Helpers.
+ // -------------------------------------------------------------------------------------------
+
+ /** Arm through {@code aere.falcon.forkBlock}, the trigger AERE-PQC-REG-ARM-01 also watches. */
+ private void armWithForkBlock(final Path registry) {
+ System.setProperty("aere.falcon.registry", registry.toAbsolutePath().toString());
+ System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
+ // N=4 is below the blocking minimum; this fixture is an isolated network and says so with the
+ // switch the codebase already uses for exactly that, rather than by pretending to be seven.
+ System.setProperty("aere.falcon.testnetAllowSmallFleet", "true");
+ }
+
+ /**
+ * Arm through the CERTIFICATE ANCHOR only, leaving {@code aere.falcon.forkBlock} unset. The
+ * threshold is 2, which {@code worstCaseKeyedSigners(4, 4)} = 3 guarantees, so the D-078 guard
+ * next door stays silent and cannot be mistaken for this one.
+ */
+ private void armWithAnchorOnly() {
+ System.setProperty(PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, Long.toString(FORK));
+ System.setProperty(PqAnchorConfig.PROPERTY_CHAIN_ID, Long.toString(CHAIN_ID));
+ System.setProperty(PqAnchorConfig.PROPERTY_MIN_SEALS, FORK + ":0," + (FORK + 10L) + ":2");
+ System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
+ System.setProperty("aere.falcon.testnetAllowSmallFleet", "true");
+ }
+
+ /**
+ * Write a registry in the legacy properties form. {@code withPossession} and {@code withClaim} are
+ * separate so that the half-bound case can be built, which is the one the loader must refuse.
+ */
+ private Path writeRegistry(
+ final String name, final boolean withPossession, final boolean withClaim) throws Exception {
+ final StringBuilder b = new StringBuilder();
+ if (withPossession || withClaim) {
+ b.append("formatVersion=").append(PqRegistryBinding.FORMAT_VERSION).append('\n');
+ b.append("chainId=").append(CHAIN_ID).append('\n');
+ b.append("bindHeight=").append(BIND_HEIGHT).append('\n');
+ }
+ b.append("count=").append(N).append('\n');
+ for (int i = 0; i < N; i++) {
+ b.append(i).append('=').append(unprefixed(PqV2Fixture.publicKey(i))).append('\n');
+ b.append(i)
+ .append(".addr=")
+ .append(unprefixed(PqV2Fixture.address(i).getBytes().toArray()))
+ .append('\n');
+ if (withPossession) {
+ b.append(i)
+ .append(".pop=")
+ .append(strip(PqV2Fixture.popHex(CHAIN_ID, BIND_HEIGHT, N, i)))
+ .append('\n');
+ }
+ if (withClaim) {
+ b.append(i)
+ .append(".claim=")
+ .append(strip(PqV2Fixture.claimHex(CHAIN_ID, BIND_HEIGHT, N, i)))
+ .append('\n');
+ }
+ }
+ final Path f = tmp.resolve(name);
+ Files.writeString(f, b.toString(), StandardCharsets.UTF_8);
+ return f;
+ }
+
+ private static String unprefixed(final byte[] b) {
+ return Bytes.wrap(b).toUnprefixedHexString();
+ }
+
+ private static String strip(final String hex) {
+ return hex.startsWith("0x") ? hex.substring(2) : hex;
+ }
+
+ /** Every system property name with the given prefix, so a precondition can be MEASURED. */
+ private static List MEASURED 2026-08-06, and it is the reason this method exists rather than being assumed
+ * unnecessary. {@code PqAnchorProducer.config()} memoises the first configuration it ever builds,
+ * for the life of the JVM. That is CORRECT in production - a node is one JVM with one set of
+ * properties, and a configuration that could change underneath the consensus path would be worse
+ * than one that cannot. In a test JVM shared by every class in this module it means an anchor
+ * armed by an earlier test is still armed here, and {@link
+ * #withNothingArmedTheGateIsInertOverTheSameV1Registry} failed exactly that way before this call
+ * was added: the property sweep found no {@code aere.pq.*} and the node still refused, because
+ * the memo held another class's anchor.
+ */
+ private static void forgetAnchorConfig() {
+ org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.useConfigForTesting(
+ null);
+ }
+
+ private static void resetFalconSingleton() throws Exception {
+ final Field f = FalconSealSupport.class.getDeclaredField("instance");
+ f.setAccessible(true);
+ f.set(null, null);
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D147InertBinaryTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D147InertBinaryTest.java
new file mode 100644
index 0000000..68737f3
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D147InertBinaryTest.java
@@ -0,0 +1,466 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
+import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.ethereum.ProtocolContext;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.Level;
+import org.apache.tuweni.bytes.Bytes32;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.quality.Strictness;
+
+/**
+ * THE COMPATIBILITY PROPERTY, which is the one that decides whether any of this can be shipped.
+ *
+ * The three anchor patches plus the D-146 arming gate are meant to travel onto the seven live
+ * boxes BEFORE the activation height, so that the fleet is already running the binary when the
+ * height arrives and activation is a restart-free event. That plan is only sound if a node holding
+ * this binary and NO {@code aere.pq.*} configuration is indistinguishable from one holding the
+ * binary it replaces: it must start, it must produce blocks, and it must not say a word about an
+ * anchor that is not armed. If that property is lost, the whole package is unusable regardless of
+ * how correct the anchor logic is, because it could not be staged.
+ *
+ * WHY THE SILENCE IS MEASURED AND NOT ASSUMED. "It returns early, so it cannot log" is a reading
+ * of the code, not a measurement, and the integrated tree has four patches whose log sites nobody
+ * has looked at together. Here the actual Log4j2 pipeline is tapped and the lines are counted.
+ *
+ * WHY {@link #positiveControlTheCaptorSEESTheAnchorWhenItISArmed} is not optional. A captor that
+ * attaches to nothing reports silence forever, and every assertion in {@link
+ * #withNoAerePropertiesTheProposerProducesABlockAndSaysNOTHING} would pass against a broken tap.
+ * The positive control arms the anchor and requires that the SAME captor, in the same JVM, sees the
+ * producer's activation line. Without it this class would be a proof that cannot go red.
+ *
+ * WHY THE CAPTOR IS BUILT BY REFLECTION. {@code log4j-core}, which owns the appender API, is on
+ * this module's RUNTIME test classpath but not its COMPILE one - measured, not assumed. Adding it as
+ * a compile dependency would put a build file into the AERE overlay, which until now is Java only.
+ * Reflection keeps the overlay unchanged, and the positive control is what makes it safe: if any of
+ * the reflective steps silently failed, the captor would see nothing and the positive control would
+ * be the test that fails.
+ *
+ * NOT MEASURED, and written rather than implied: nothing is deployed and no node is started. That
+ * an unarmed node on one of the seven real boxes behaves this way over a real chain, at 523 ms
+ * blocks, alongside a peer that IS armed, is NOT MEASURED and needs the rehearsal network.
+ */
+public class D147InertBinaryTest {
+
+ /**
+ * Loggers that exist ONLY because of the anchor work, so any line from them on an unarmed node is
+ * by itself a finding.
+ *
+ * {@code FalconSealSupport} is deliberately NOT here even though it is the loudest of them.
+ * It predates the anchor and legitimately says one thing at startup; listing it would make the
+ * filter report a four-year-old INFO line as new anchor chatter. Its armed messages are caught by
+ * {@link #ANCHOR_WORDS} instead, which keys on what the line SAYS rather than who said it.
+ */
+ private static final List MEASURED 2026-08-06, and it is the reason this constant exists rather than an {@code
+ * isEmpty()} on everything. The first shape of this test asserted total silence and went red on
+ * this line. It is not a regression: {@code git log -S} places it in commit 307fd0d0, the snapshot
+ * of everything built between 14 June and 2 August, so it predates all three anchor patches and
+ * the arming gate. It is {@code LOG.info} and it says the node has no Falcon registry, which is
+ * true and was equally true of the binary being replaced.
+ *
+ * So the property that is actually worth defending is not "says nothing" - that was never true
+ * - but "says nothing NEW, and nothing about the anchor". Pinning the exact text is what makes the
+ * second half enforceable: a fourth patch that adds one more startup line has to come here and
+ * change this constant deliberately.
+ */
+ private static final String THE_ONE_PRE_EXISTING_LINE =
+ "AERE PQC: no Falcon registry configured "
+ + "(aere.falcon.genesis/aere.falcon.manifest/aere.falcon.registry); "
+ + "hybrid seal verification will be a no-op.";
+
+ private static final long CHAIN_ID = 220_878L;
+
+ private static final long H = 4_000L;
+
+ /**
+ * Every property this class may touch. The unarmed test does not rely on this list - it sweeps the
+ * whole property table - but the armed one must put back exactly what it took.
+ */
+ private static final List The block-production half is asserted on OBJECT IDENTITY, not equality. {@code
+ * PqAnchorProducer.apply} returns its argument unchanged at the first branch when the anchor is
+ * not active; an equal-but-rebuilt {@code BftExtraData} would mean the producer had walked the
+ * certificate path and merely arrived back at the same value, which is a different and much
+ * weaker statement.
+ */
+ @Test
+ public void withNoAerePropertiesTheProposerProducesABlockAndSaysNOTHING() throws Exception {
+ // The precondition is MEASURED over the whole property table rather than trusted to the
+ // teardown of whatever test ran before this one in this JVM.
+ assertThat(systemPropertiesStartingWith("aere."))
+ .describedAs("the precondition of this test, measured rather than assumed")
+ .isEmpty();
+
+ final LogCaptor captor = LogCaptor.attach();
+ final BftExtraData produced;
+ final BftExtraData base = plainExtraData();
+ try {
+ assertThatCode(FalconSealSupport::instance)
+ .describedAs("a box with no key ceremony behind it must still come up")
+ .doesNotThrowAnyException();
+
+ final BlockHeader parent = new BlockHeaderTestFixture().number(H + 500L).buildHeader();
+ produced = PqAnchorProducer.apply(base, parent, contextWith(List.of()));
+ } finally {
+ captor.detach();
+ }
+
+ assertThat(produced)
+ .describedAs(
+ "the unarmed producer must hand back the very object it was given; an equal copy would "
+ + "mean it had walked the certificate path")
+ .isSameAs(base);
+
+ assertThat(PqAnchorProducer.config().everActive())
+ .describedAs("and it must consider itself never-active, not merely inactive right now")
+ .isFalse();
+
+ assertThat(captor.anchorLines())
+ .describedAs(
+ "an operator staging this binary before the height must see NOTHING about the anchor; "
+ + "%d line(s) in total were seen, so the captor was live",
+ captor.total())
+ .isEmpty();
+
+ // And nothing NEW of any kind. This is the half that catches a future patch adding chatter.
+ assertThat(captor.aereLines())
+ .describedAs(
+ "the whole AERE output of an unarmed node, pinned: exactly the one INFO line that "
+ + "predates these patches (commit 307fd0d0). A new line here is a staging "
+ + "regression even when it is harmless, because it changes what the fleet prints "
+ + "on a restart that is supposed to be a no-op.")
+ .containsExactly(THE_ONE_PRE_EXISTING_LINE);
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // THE POSITIVE CONTROL, without which the test above proves nothing.
+ // ---------------------------------------------------------------------------------------------
+
+ /**
+ * The same captor, the same JVM, the same loggers - with the anchor armed. If this does not see a
+ * line, the silence measured above is the silence of a broken tap and means nothing.
+ *
+ * The line chosen is the producer's own activation notice, emitted from {@code
+ * PqAnchorProducer.config()} the first time a configuration is built. Its once-per-JVM latch is
+ * reset by {@code useConfigForTesting(null)}, which is why {@link #forgetAnchorConfig()} runs
+ * before every test in this class.
+ */
+ @Test
+ public void positiveControlTheCaptorSEESTheAnchorWhenItISArmed() throws Exception {
+ System.setProperty(PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, Long.toString(H));
+ System.setProperty(PqAnchorConfig.PROPERTY_CHAIN_ID, Long.toString(CHAIN_ID));
+ // A whole-zero schedule is refused since the D-147 floor (armed anchor, no signature
+ // requirement, for ever). The warm-up step at H stays 0; the rise satisfies the floor.
+ System.setProperty(PqAnchorConfig.PROPERTY_MIN_SEALS, H + ":0," + (H + 21_600L) + ":3");
+
+ final LogCaptor captor = LogCaptor.attach();
+ try {
+ PqAnchorProducer.config();
+ } finally {
+ captor.detach();
+ }
+
+ assertThat(captor.anchorLines())
+ .describedAs(
+ "the captor must be able to hear the anchor, or the silence next door is worthless")
+ .isNotEmpty();
+ assertThat(String.join("\n", captor.anchorLines())).contains("producer armed");
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // Helpers.
+ // ---------------------------------------------------------------------------------------------
+
+ /** Extra data with no anchor digest, i.e. exactly what a pre-fork proposer builds. */
+ private static BftExtraData plainExtraData() {
+ return new BftExtraData(
+ Bytes32.ZERO,
+ Collections.emptyList(),
+ Optional.empty(),
+ 0,
+ Collections.emptyList(),
+ Collections.emptyList());
+ }
+
+ private static ProtocolContext contextWith(final Collection {@link #attach()} throws if any reflective step fails. It does NOT fall back to a silent
+ * captor: a captor that quietly captures nothing is precisely the failure this class is written to
+ * exclude.
+ */
+ private static final class LogCaptor {
+
+ private final List These tests exercise the single choke point every configured value passes through
+ * ({@link FalconSealSupport#resolve}), so the four behaviours are proven once for all sixteen
+ * switches instead of sixteen times over.
+ */
+class D177NeutralNamesTest {
+
+ private static final String LEGACY = "aere.falcon.validatorCount";
+ private static final String NEUTRAL = "aere.pq.sig.validatorCount";
+ private static final String ENV = "AERE_FALCON_VALIDATOR_COUNT";
+
+ @AfterEach
+ void clear() {
+ System.clearProperty(LEGACY);
+ System.clearProperty(NEUTRAL);
+ }
+
+ @Test
+ void neutralNameAloneIsRead() {
+ System.setProperty(NEUTRAL, "9");
+ assertThat(FalconSealSupport.resolve(LEGACY, ENV)).isEqualTo("9");
+ }
+
+ @Test
+ void legacyNameAloneStillWorks() {
+ System.setProperty(LEGACY, "7");
+ assertThat(FalconSealSupport.resolve(LEGACY, ENV)).isEqualTo("7");
+ }
+
+ @Test
+ void bothNamesSameValueIsAMigrationWindow() {
+ System.setProperty(NEUTRAL, "9");
+ System.setProperty(LEGACY, "9");
+ assertThat(FalconSealSupport.resolve(LEGACY, ENV)).isEqualTo("9");
+ }
+
+ @Test
+ void bothNamesDifferentValuesRefuseLoudly() {
+ System.setProperty(NEUTRAL, "9");
+ System.setProperty(LEGACY, "7");
+ assertThatThrownBy(() -> FalconSealSupport.resolve(LEGACY, ENV))
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("AERE-PQC-CFG-DUAL-NAME-01")
+ .hasMessageContaining(NEUTRAL)
+ .hasMessageContaining(LEGACY);
+ }
+
+ @Test
+ void neutralNameWinsWhenBothAreSemanticallyEqual() {
+ // same text with different whitespace: trim makes them equal, and the neutral value is the
+ // one returned, so new fleets can write only the neutral name with no surprise
+ System.setProperty(NEUTRAL, " 9 ");
+ System.setProperty(LEGACY, "9");
+ assertThat(FalconSealSupport.resolve(LEGACY, ENV)).isEqualTo(" 9 ");
+ }
+
+ @Test
+ void nonFalconPropertiesAreLeftUntouched() {
+ // a property that does not start with aere.falcon. gets no twin: resolve stays exactly the
+ // reader it was before for it
+ System.setProperty("aere.pq.anchorBlock", "13014000");
+ try {
+ assertThat(FalconSealSupport.resolve("aere.pq.anchorBlock", "AERE_PQ_ANCHOR_BLOCK"))
+ .isEqualTo("13014000");
+ } finally {
+ System.clearProperty("aere.pq.anchorBlock");
+ }
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D2CallerIntentTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D2CallerIntentTest.java
new file mode 100644
index 0000000..cc1ce06
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D2CallerIntentTest.java
@@ -0,0 +1,386 @@
+/*
+ * 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.LinkedHashMap;
+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;
+
+/**
+ * D2 HARDENING (b-v2). The repair of the repair: the caller's MOTIVE decides, not the height.
+ *
+ * WHAT THE FIRST SHAPE DID, MEASURED AND NOT ARGUED. On 2026-08-06 hardening (b) refused every
+ * unbound height at or above the arming height, deciding from the block NUMBER alone. Run against
+ * the suite that gave 588 tests and 0 failures on a clean tree, it gave 597 tests and 6 failures:
+ * five in {@code D141SealPersistenceTest} and one in {@code D078ValidatorSetChangeTest}. Both
+ * classes work on THIS NODE'S OWN head - restarting and re-reading its own seal file, and proposing
+ * on top of its own head - and in all six the number handed to the guard was 1030 with an arming
+ * height of 1000. A genuinely historical question, in the same process in the same second, hands
+ * the guard exactly those numbers too. No arithmetic on the height separates them.
+ *
+ * THE OPERATIONAL CONSEQUENCE, in the words of the D078 failure itself: {@code refusing to
+ * propose on top of block 1030 because this node holds 0 valid eligible Falcon seal(s)}. The first
+ * shape turned a defect that is invisible on a running fleet and fatal only to a node syncing later
+ * into one that stops block production on all seven, in the minute the anchor is armed.
+ *
+ * WHAT SEPARATES THEM IS WHO SUPPLIES THE SUBJECT, and that is known at every call site and was
+ * being discarded at the interface boundary. So {@code PqSignerRegistry} now carries two named
+ * pairs, and the compiler forces every call site to say which question it is asking. This class is
+ * the proof that the two doors answer DIFFERENTLY at the SAME height, that the own-head door is not
+ * a loophole, and that the history door still refuses.
+ *
+ * THIS CLASS CANNOT GO GREEN BY ACCIDENT. Three of its tests fail if the own-head door is made
+ * to refuse (which is the first shape restored), and three fail if the history door is made to
+ * answer (which is the pre-2026-08-06 defect restored). The two plants are run in opposite
+ * directions and both are recorded in the evidence directory.
+ */
+public class D2CallerIntentTest {
+
+ /** Anchor activation height H, matching the fixture the six failures ran under. */
+ 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;
+
+ /**
+ * The height the six failures actually presented to the guard: this node's own head, above the
+ * arming height. Named for what it is, because the whole point is that the NUMBER is innocent.
+ */
+ private static final long OWN_HEAD = 1_030L;
+
+ /** A height far above H, standing in for "a year of history above the arming height". */
+ private static final long DEEP = K_AT + 5_000L;
+
+ private static final int N = 7;
+
+ private static final long CHAIN_ID = 220_878L;
+
+ @TempDir private Path tmp;
+
+ private final List WHAT THE DOSSIER MEASURED. {@code PqSignerRegistry} had {@code addressForIndex(int)} and {@code
+ * verify(int, Bytes, Bytes)} with no height, and {@code FalconSealSupport} held ONE registry loaded
+ * at start-up. So a header that passed both anchor rules was REJECTED the moment index 0's Falcon
+ * key was rotated - same header, same parent, same validator set.
+ *
+ * WHAT WAS REPAIRED BEFORE THIS FILE, AND WHAT WAS NOT. Commit f3ebe90c (D-081) gave the
+ * validation path {@code addressForIndexAt} / {@code verifyAt} and a height-indexed schedule. The
+ * measurement of 2026-08-05 found the repair INERT, for a reason that is one line long: with no
+ * {@code config.pqRegistryHash} in genesis - and there is none in any genesis this fleet runs -
+ * {@code keyAt} fell back to the registry in force AT THE HEAD, at every height. Height-aware
+ * signatures, head-registry answers. T2 stood exactly as measured.
+ *
+ * WHAT THIS FILE ASSERTS, as a property and not as a scenario: at and above the arming height,
+ * a node that cannot say which key set was in force must REFUSE, not guess. Below the arming
+ * height it must keep answering from the head registry, because nothing there is being judged and
+ * the 11.8 million blocks already on chain 2800 must behave bit for bit as they did.
+ *
+ * THE NEGATIVE CONTROL IS BUILT IN, not promised. {@link
+ * #belowTheArmingHeightTheHeadRegistryStillAnswers()} fails if the refusal is made unconditional;
+ * {@link #whenTheAnchorIsNotArmedNOTHINGCHANGES()} fails if it is made independent of arming; {@link
+ * #withTheScheduleConfiguredTheArmedHeightsAnswerAgain()} fails if the refusal is anything other
+ * than a missing height-to-registry binding. And the measurement itself, {@link
+ * #d2t2AtAndAboveTheArmingHeightWithNoScheduleTheAnswerIsRefusal()}, is GREEN on the unrepaired code
+ * only if the fallback is restored - which is exactly the one-line edit the repair removed.
+ */
+public class D2RegistryHeightRefusalTest {
+
+ /** Anchor activation height H: from here a header's Falcon certificate carries weight. */
+ 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;
+
+ /** A height far above H, standing in for "a year of history above the arming height". */
+ private static final long DEEP = K_AT + 5_000L;
+
+ private static final int N = 7;
+
+ private static final long CHAIN_ID = 220_878L;
+
+ @TempDir private Path tmp;
+
+ private final List The sequencing rule is asserted as a RULE, not as a count. Until 2026-08-29 this test pinned
+ * the literal phrase "At quorum 5 of 7 you lose the chain", which had been false since the set
+ * grew to nine on 2026-08-12: the message, and this test with it, carried the fleet of a world
+ * three weeks gone. A message that names today's set size is wrong on the day it changes, and the
+ * test that pins it makes the wrongness load-bearing.
*/
@Test
public void theRefusalMessageCarriesEverythingAnOperatorNeedsAtThreeInTheMorning() {
@@ -676,7 +681,8 @@ public class PqAnchorConfigTest {
.hasMessageContaining("a step exactly at " + H)
.hasMessageContaining("FIX correct BESU_OPTS on THIS node")
.hasMessageContaining("restart one at a time")
- .hasMessageContaining("At quorum 5 of 7 you lose the chain")
+ .hasMessageContaining("never in parallel")
+ .hasMessageContaining("more than f")
.hasMessageContaining("EMERGENCY " + PqAnchorConfig.PROPERTY_DISABLE + "=true");
}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java
index 222f45b..67fb25b 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorIntervalTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorIntervalTest.java
index 08afdba..d1cd9fd 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorIntervalTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorIntervalTest.java
@@ -2,10 +2,8 @@
* AERE, 2026-08-07. The anchor interval: a certificate on every Nth block instead of every block.
*
* WHY THIS EXISTS. At ~523 ms per block we produce 165,248 blocks per day, 23 times more than
- * Ethereum. A certificate in EVERY block multiplies the header by almost five even with the cap at
- * K=3, and on a chain with empty blocks the headers are close to everything that gets written to
- * disk. So that multiplier is the multiplier of database growth, and the design does not fit on a
- * reasonably provisioned node. The interval divides it by N.
+ * Ethereum. A certificate in EVERY block costs 120.5 GB per year per node even with the cap at K=3.
+ * The fleet's disks are 38 and 75 GB, so the design does not fit anywhere.
*
* WHY IT IS SAFE, and this is the argument that has to hold, not the saving. Block hashes chain:
* block N+1 commits to the hash of N. So an anchor at height A, whose vanityData binds a Falcon
@@ -132,10 +130,10 @@ class PqAnchorIntervalTest {
// ---------------------------------------------------------------------------------------------
@Test
void aNodeWithNoAnchorAtAllHasNoAnchorHeights() {
- final PqAnchorConfig niciodata = PqAnchorConfig.never(2800L).withAnchorInterval(OptionalInt.of(100));
+ final PqAnchorConfig never = PqAnchorConfig.never(2800L).withAnchorInterval(OptionalInt.of(100));
for (final long n : new long[] {0L, 1L, H, H + 100, Long.MAX_VALUE - 1}) {
- assertThat(niciodata.isAnchorHeight(n)).as("height %d", n).isFalse();
- assertThat(niciodata.anchorAppliesAt(n)).as("height %d", n).isFalse();
+ assertThat(never.isAnchorHeight(n)).as("height %d", n).isFalse();
+ assertThat(never.anchorAppliesAt(n)).as("height %d", n).isFalse();
}
}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java
index 53917ee..651c63b 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java
@@ -1,16 +1,15 @@
/*
- * AERE, 2026-08-07. THE THRESHOLD FLOOR: a schedule whose effective K is zero everywhere leaves the
- * anchor armed and completely toothless, forever, and every tool reports GREEN the whole time it is
- * happening, because they all measure what was ASKED FOR and the request is valid.
+ * AERE D-147, 2026-08-07. THE THRESHOLD FLOOR: a schedule whose effective K is zero everywhere
+ * leaves the anchor armed and completely toothless, forever, and every tool reports GREEN the whole
+ * time it is happening, because they all measure what was ASKED FOR and the request is valid.
*
* The loader already guarded this consequence in its own words, "K would be 0 at every height
* and an ARMED node would accept empty certificates", but only for a MISSING schedule. A schedule
* that is PRESENT and of the form " THE LEAK, paid for twice. {@code PqAnchorProducer.config()} memoizes the first configuration
+ * it builds, and that memo outlives every {@code System.clearProperty} a test class runs in its
+ * teardown. A class that arms the anchor through system properties and then builds {@code
+ * FalconSealSupport} caches an ARMED config for whichever class runs next in the same JVM. Measured
+ * 2026-08-11: {@code PqForkThresholdReachabilityTest} left exactly this behind and four
+ * PqStartupHistoryTest tests failed on a guard firing correctly; that class got the cleanup line.
+ * Measured 2026-08-20 on the production tree: its fork, {@code D078ThresholdReachabilityTest},
+ * never received the same line, and all five {@code PqFleetRestartArmingTest} fixtures turned into
+ * AERE-PQC-REG-ARM-02 refusals -- green alone, red in the full suite, identical sources.
+ *
+ * WHY THIS TEST IS SHAPED LIKE THIS. Class-order contamination is nondeterministic under
+ * gradle's fork assignment, so the reproduction does not rely on ordering at all: it runs the
+ * guilty class's OWN lifecycle (setUp, the arming test, tearDown) inside one test method, and then
+ * asserts the JVM is clean. If the cleanup line is ever removed from that teardown again, this
+ * test goes red deterministically -- that removal is exactly the planted failure it was proven
+ * against on the day it was written.
+ */
+public class PqAnchorProducerCacheHygieneTest {
+
+ @TempDir private Path tmp;
+
+ @BeforeEach
+ public void curatInainte() throws Exception {
+ curata();
+ }
+
+ @AfterEach
+ public void curatDupa() throws Exception {
+ curata();
+ }
+
+ private static void curata() throws Exception {
+ for (final String p : System.getProperties().stringPropertyNames()) {
+ if (p.startsWith("aere.")) {
+ System.clearProperty(p);
+ }
+ }
+ PqAnchorProducer.useConfigForTesting(null);
+ final Field f = FalconSealSupport.class.getDeclaredField("instance");
+ f.setAccessible(true);
+ f.set(null, null);
+ }
+
+ @Test
+ public void theReachabilitySequenceLeavesNoArmedAnchorBehind() throws Exception {
+ final D078ThresholdReachabilityTest vinovat = new D078ThresholdReachabilityTest();
+ final Field tmpField = D078ThresholdReachabilityTest.class.getDeclaredField("tmp");
+ tmpField.setAccessible(true);
+ tmpField.set(vinovat, tmp);
+
+ vinovat.setUp();
+ try {
+ // The exact sequence that poisons: anchor armed from properties, FalconSealSupport built.
+ vinovat.aReachableThresholdMustStillStart();
+ } finally {
+ // The guilty class's OWN teardown. The assertion below is about what IT leaves behind.
+ vinovat.tearDown();
+ }
+
+ assertThat(PqAnchorProducer.config().everActive())
+ .describedAs(
+ "after D078ThresholdReachabilityTest's own teardown, a config built in this JVM must "
+ + "not claim an armed anchor; if it does, the per-JVM cache survived the cleanup "
+ + "and every proof-less fixture in the next class dies with AERE-PQC-REG-ARM-02")
+ .isFalse();
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCostTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCostTest.java
index 252fd34..d337674 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCostTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCostTest.java
@@ -4,9 +4,8 @@
* WHY THIS EXISTS. On 7 August `aere.pq.anchor.maxSeals` and `aere.pq.anchorInterval` were built,
* and their configuration guards were proven the same day. But the cut in the producer, the code
* that ACTUALLY stops seals being written past the cap, and that ACTUALLY skips the heights with no
- * anchor, stayed an ASSERTION: there was no producer harness in the tree, and we had just seen, at
- * the wiring of the rules into the validation chain, what a piece of code that no proof touches
- * costs.
+ * anchor, stayed an ASSERTION: there was no producer harness in the tree, and D-148 had just shown
+ * what a piece of code that no proof touches costs.
*
* This class touches it. It counts the seals written, it does not assume them.
*
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java
index 5c61464..c9bfd2e 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java
@@ -3,9 +3,9 @@
*
* WHY IT EXISTS. K is a FLOOR, not a cap. Measured on a live ten-node run with the threshold at 4:
* 42 blocks carried 4 seals, 36 carried 5, 5 carried 6. The proposer writes every seal it heard and
- * that is eligible, not as many as the threshold demands. At 666 bytes a seal, that means about two
- * thirds more header written than the threshold asks for, and the surplus buys nothing: what a
- * verifier demands is THE THRESHOLD.
+ * that is eligible, not as many as the threshold demands. At 666 bytes a seal, that means 200.9 GB
+ * per node per year instead of 120.5, and the surplus buys nothing: what a verifier demands is THE
+ * THRESHOLD.
*
* WHAT THIS FILE GUARDS, and this is the dangerous part: a cap set BELOW the highest K in the
* schedule makes the proposer write certificates its own fleet rejects, at every height from the
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java
index 0e65d7f..5bea4d3 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuardTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuardTest.java
index 9c7be44..ebf7975 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuardTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuardTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -27,9 +27,10 @@ import org.junit.jupiter.api.Test;
* AERE GARDA-PRAG: the PLANTED FAILURE for the seal-threshold guard.
*
* Each refusal test builds, by hand, the exact configuration that stops the chain, and asserts
- * that the guard sees it. The bound asserted is {@code K <= quorum(N) - 1}, which is 4 at N=7 and 2
- * at N=4, and {@link #growingTheValidatorSetDoesNotBuyQuorumMargin()} is the test that would go green
- * under the WRONG bound {@code K > N - f} and red under the right one.
+ * that the guard sees it. REVISED 2026-08-20: the bound asserted is {@code K <= N - f} (availability
+ * under the fault budget), which is 5 at N=7 and 3 at N=4. Until D-227 the bound was
+ * {@code quorum - 1}, and {@link #atNineValidatorsTheQuorumIsReachableAndAboveNMinusFIsNot()}
+ * carries the dated history of that reversal, with the measurement that forced it.
*
* The negative control for this file does not live in it: it is a second build of the same tree in
* which the guard body is replaced by a stub that accepts everything. Every refusal assertion below
@@ -49,27 +50,30 @@ class PqAnchorThresholdGuardTest {
@Test
void theArithmeticIsTheOneTheChainActuallyUses() {
- // The bound is not a constant typed into this test: it is Besu's own quorum formula, minus one.
+ // REVISED 2026-08-20 with the D-227 doctrine: the bound is N - f (availability under the fault
+ // budget), no longer quorum - 1 (the pre-salvage gathering ceiling). Still not a constant typed
+ // here: quorum comes from Besu's own formula, f from the guard's own budget.
assertThat(BftHelpers.calculateRequiredValidatorQuorum(N_LIVE)).isEqualTo(5);
- assertThat(PqAnchorThresholdGuard.maxConfigurableThreshold(N_LIVE)).isEqualTo(4);
+ assertThat(PqAnchorThresholdGuard.maxConfigurableThreshold(N_LIVE)).isEqualTo(5);
assertThat(PqAnchorThresholdGuard.byzantineBudget(N_LIVE)).isEqualTo(2);
assertThat(BftHelpers.calculateRequiredValidatorQuorum(4)).isEqualTo(3);
- assertThat(PqAnchorThresholdGuard.maxConfigurableThreshold(4)).isEqualTo(2);
+ assertThat(PqAnchorThresholdGuard.maxConfigurableThreshold(4)).isEqualTo(3);
}
@Test
- void plantedFailureAThresholdEqualToTheQuorumIsRefused() {
+ void plantedFailureAThresholdAboveNMinusFIsRefused() {
+ // At N=7, N - f = 5, so 6 is the first fatal rung: with f=2 validators down only 5 seals exist.
assertThatThrownBy(
() ->
PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
- armed(Map.of(H, 0, H + 7_200L, 1, H + 21_600L, 5), OptionalInt.empty()),
+ armed(Map.of(H, 0, H + 7_200L, 1, H + 21_600L, 6), OptionalInt.empty()),
N_LIVE))
.isInstanceOf(FalconSealSupport.ActivationConfigException.class)
.hasMessageContaining(PqAnchorThresholdGuard.CODE)
.hasMessageContaining("REFUSING TO START")
- .hasMessageContaining("reaches 5 at height " + (H + 21_600L))
- .hasMessageContaining("may be configured at this set size is 4");
+ .hasMessageContaining("reaches 6 at height " + (H + 21_600L))
+ .hasMessageContaining("may be configured at this set size is 5");
}
@Test
@@ -84,14 +88,14 @@ class PqAnchorThresholdGuardTest {
@Test
void plantedFailureTheVeryFirstStepMayAlsoBeFatal() {
- // A schedule that opens AT the quorum. The producer's existing log-only warning covers K>0 at H
+ // A schedule that opens ABOVE N - f. The producer's existing log-only warning covers K>0 at H
// for a different reason; this asserts the refusal fires on the same step.
assertThatThrownBy(
() ->
PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
- armed(Map.of(H, 5), OptionalInt.empty()), N_LIVE))
+ armed(Map.of(H, 6), OptionalInt.empty()), N_LIVE))
.isInstanceOf(FalconSealSupport.ActivationConfigException.class)
- .hasMessageContaining("reaches 5 at height " + H);
+ .hasMessageContaining("reaches 6 at height " + H);
}
@Test
@@ -99,7 +103,7 @@ class PqAnchorThresholdGuardTest {
assertThatCode(
() ->
PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
- armed(Map.of(H, 0, H + 7_200L, 1, H + 21_600L, 4), OptionalInt.empty()),
+ armed(Map.of(H, 0, H + 7_200L, 1, H + 21_600L, 5), OptionalInt.empty()),
N_LIVE))
.doesNotThrowAnyException();
}
@@ -115,23 +119,33 @@ class PqAnchorThresholdGuardTest {
}
@Test
- void growingTheValidatorSetDoesNotBuyQuorumMargin() {
- // THIS is the test that separates the right bound from the wrong one. At N=9 the quorum is 6
- // while N-f is 7, so the rule "refuse when K > N - f" would ACCEPT K=7, which is a rung no
- // proposer can ever reach. Both 6 and 7 must be refused.
+ void atNineValidatorsTheQuorumIsReachableAndAboveNMinusFIsNot() {
+ // HISTORY, kept on purpose: until 2026-08-20 this test was named
+ // growingTheValidatorSetDoesNotBuyQuorumMargin and asserted that K=6 and K=7 are both refused
+ // at N=9, because pre-D-227 a proposer could gather at most quorum seals. D-227's late-seal
+ // salvage changed the physics (mainnet measurement: 8-9 seals per certificate across 5,400
+ // anchors), so growing the set NOW buys reachable rungs. The fatal bound is availability under
+ // the fault budget: N - f = 7 at N=9. 6 and 7 start (loudly); 8 is refused.
assertThat(BftHelpers.calculateRequiredValidatorQuorum(9)).isEqualTo(6);
assertThat(9 - PqAnchorThresholdGuard.byzantineBudget(9)).isEqualTo(7);
+ assertThat(PqAnchorThresholdGuard.maxConfigurableThreshold(9)).isEqualTo(7);
- assertThatThrownBy(
+ assertThatCode(
() ->
PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
armed(Map.of(H, 0, H + 100L, 6), OptionalInt.empty()), 9))
- .isInstanceOf(FalconSealSupport.ActivationConfigException.class);
+ .doesNotThrowAnyException();
+
+ assertThatCode(
+ () ->
+ PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
+ armed(Map.of(H, 0, H + 100L, 7), OptionalInt.empty()), 9))
+ .doesNotThrowAnyException();
assertThatThrownBy(
() ->
PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
- armed(Map.of(H, 0, H + 100L, 7), OptionalInt.empty()), 9))
+ armed(Map.of(H, 0, H + 100L, 8), OptionalInt.empty()), 9))
.isInstanceOf(FalconSealSupport.ActivationConfigException.class);
assertThatCode(
@@ -142,18 +156,20 @@ class PqAnchorThresholdGuardTest {
}
@Test
- void theBoundAtFourValidatorsIsTwo() {
+ void theBoundAtFourValidatorsIsThree() {
+ // N=4: f=1, N-f=3. K=3 (the full quorum) starts; K=4 demands a seal from every validator
+ // including the one the fault budget says may be down, and is refused.
assertThatThrownBy(
() ->
PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
- armed(Map.of(H, 0, H + 30L, 3), OptionalInt.empty()), 4))
+ armed(Map.of(H, 0, H + 30L, 4), OptionalInt.empty()), 4))
.isInstanceOf(FalconSealSupport.ActivationConfigException.class)
.hasMessageContaining("quorum for the 4 validators");
assertThatCode(
() ->
PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
- armed(Map.of(H, 0, H + 30L, 2), OptionalInt.empty()), 4))
+ armed(Map.of(H, 0, H + 30L, 3), OptionalInt.empty()), 4))
.doesNotThrowAnyException();
}
@@ -163,17 +179,18 @@ class PqAnchorThresholdGuardTest {
assertThatCode(
() ->
PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
- armed(Map.of(H, 0, H + 21_600L, 5), OptionalInt.of(1)), N_LIVE))
+ armed(Map.of(H, 0, H + 21_600L, 6), OptionalInt.of(1)), N_LIVE))
.doesNotThrowAnyException();
}
@Test
void aCeilingAboveTheScheduleRescuesNothing() {
- // The ceiling can only ever lower. A ceiling of 9 over a fatal 5 leaves the 5 in force.
+ // The ceiling can only ever lower. A ceiling of 9 over a fatal 6 leaves the 6 in force.
+ // (5 stopped being fatal at N=7 with the 2026-08-20 doctrine: N - f = 5 is now the bound.)
assertThatThrownBy(
() ->
PqAnchorThresholdGuard.verifyThresholdAgainstQuorumOrAbort(
- armed(Map.of(H, 0, H + 21_600L, 5), OptionalInt.of(9)), N_LIVE))
+ armed(Map.of(H, 0, H + 21_600L, 6), OptionalInt.of(9)), N_LIVE))
.isInstanceOf(FalconSealSupport.ActivationConfigException.class);
}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2Test.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2Test.java
new file mode 100644
index 0000000..63f4f4f
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2Test.java
@@ -0,0 +1,192 @@
+/* AERE crypto-agility, step 2 proofs. The controls that matter most here are the CROSS-FORMAT
+ * ones: v2 bytes must never parse as a legacy certificate, legacy bytes must be refused BY NAME
+ * by the v2 decoder, and the two digests must never agree. A versioned format whose versions can
+ * be confused is worse than one format. */
+package org.hyperledger.besu.consensus.common.bft;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.List;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.apache.tuweni.bytes.Bytes32;
+import org.hyperledger.besu.crypto.SecureRandomProvider;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput;
+import org.hyperledger.besu.ethereum.rlp.RLPInput;
+import org.junit.jupiter.api.Test;
+
+class PqAnchorV2Test {
+
+ private static final byte FALCON = 0x01;
+ private static final byte SLHDSA = 0x02;
+ private static final Bytes SIG_A = Bytes.fromHexString("0xaaaa");
+ private static final Bytes SIG_B = Bytes.fromHexString("0xbbbb");
+ private static final Bytes32 PARENT_HASH = Bytes32.leftPad(Bytes.of(7));
+
+ private final SecureRandom random = SecureRandomProvider.createSecureRandom();
+
+ private static List WHAT THE WIRE BUYS, stated as the thing that is actually true. Before it, an ARMED node loaded
* a v1 registry without a word, and the registry decides who a Falcon seal is credited to. Measured
@@ -207,7 +207,7 @@ public class PqArmingGateTest {
+ "trigger is ARMING and not the file")
.doesNotThrowAnyException();
assertThat(FalconSealSupport.instance().registrySize())
- .describedAs("and an unarmed node's registry is loaded exactly as it was before this guard")
+ .describedAs("and an unarmed node's registry is loaded exactly as it was before D-146")
.isEqualTo(N);
}
@@ -227,8 +227,7 @@ public class PqArmingGateTest {
* An ARMED node with no registry file at all is deliberately NOT this guard's business, and this
* test is what stops that from being a silent decision.
*
- * This guard is about mis-ATTRIBUTION, which needs rows; an empty registry credits nobody. The
- * condition
+ * D-146 is mis-ATTRIBUTION, which needs rows; an empty registry credits nobody. The condition
* is owned by AERE-PQC-CFG-UNSAFE-08 when the threshold is positive, and MEASURED here: with a
* threshold of zero, which is the warm-up regime the fleet is meant to arm INTO, the node starts.
* An earlier revision of this guard refused here, and the cost was exactly that - the intended
@@ -243,7 +242,7 @@ public class PqArmingGateTest {
System.setProperty("aere.falcon.testnetAllowSmallFleet", "true");
assertThatCode(FalconSealSupport::instance)
- .describedAs("K=0 over an empty registry is the warm-up regime, not a mis-attribution defect")
+ .describedAs("K=0 over an empty registry is the warm-up regime, not a D-146 defect")
.doesNotThrowAnyException();
}
@@ -253,7 +252,7 @@ public class PqArmingGateTest {
/**
* A row that carries a Falcon possession proof and no ECDSA claim proves that SOMEBODY holds the
- * key, and says nothing about which validator asked for it - which is the whole of this guard.
+ * key, and says nothing about which validator asked for it - which is the whole of D-146.
*
* MEASURED, and the assertion was CHANGED to match the measurement rather than the other way
* round. The expectation written first was AERE-PQC-REG-ARM-02. What actually happens is a refusal
@@ -291,8 +290,8 @@ public class PqArmingGateTest {
/**
* Arm through the CERTIFICATE ANCHOR only, leaving {@code aere.falcon.forkBlock} unset. The
- * threshold is 2, which {@code worstCaseKeyedSigners(4, 4)} = 3 guarantees, so the
- * threshold-reachability guard next door stays silent and cannot be mistaken for this one.
+ * threshold is 2, which {@code worstCaseKeyedSigners(4, 4)} = 3 guarantees, so the D-078 guard
+ * next door stays silent and cannot be mistaken for this one.
*/
private void armWithAnchorOnly() {
System.setProperty(PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, Long.toString(FORK));
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqCallerIntentTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqCallerIntentTest.java
index 0af0116..3800bfe 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqCallerIntentTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqCallerIntentTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -39,7 +39,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
- * ROTATION HARDENING (b-v2). The repair of the repair: the caller's MOTIVE decides, not the height.
+ * D2 HARDENING (b-v2). The repair of the repair: the caller's MOTIVE decides, not the height.
*
* WHAT THE FIRST SHAPE DID, MEASURED AND NOT ARGUED. On 2026-08-06 hardening (b) refused every
* unbound height at or above the arming height, deciding from the block NUMBER alone. Run against
@@ -50,7 +50,7 @@ import org.junit.jupiter.api.io.TempDir;
* height of 1000. A genuinely historical question, in the same process in the same second, hands
* the guard exactly those numbers too. No arithmetic on the height separates them.
*
- * THE OPERATIONAL CONSEQUENCE, in the words of that failure itself: {@code refusing to
+ * THE OPERATIONAL CONSEQUENCE, in the words of the D078 failure itself: {@code refusing to
* propose on top of block 1030 because this node holds 0 valid eligible Falcon seal(s)}. The first
* shape turned a defect that is invisible on a running fleet and fatal only to a node syncing later
* into one that stops block production on all seven, in the minute the anchor is armed.
@@ -64,7 +64,7 @@ import org.junit.jupiter.api.io.TempDir;
* THIS CLASS CANNOT GO GREEN BY ACCIDENT. Three of its tests fail if the own-head door is made
* to refuse (which is the first shape restored), and three fail if the history door is made to
* answer (which is the pre-2026-08-06 defect restored). The two plants are run in opposite
- * directions, and each was measured before this class was allowed to count as evidence.
+ * directions and both are recorded in the evidence directory.
*/
public class PqCallerIntentTest {
@@ -100,10 +100,10 @@ public class PqCallerIntentTest {
@BeforeEach
public void setUp() throws Exception {
- // AERE REGISTRY BINDING (2026-08-06): a v2, PROOF-BOUND registry. It used to be v1, with
- // addresses spelled 0xA00+i, which no secp256k1 key can sign for, so this fixture described a
- // fleet that could never satisfy AERE-PQC-REG-ARM-02 once that guard was wired. The registry is
- // bound at H, the height this fixture arms the anchor from.
+ // AERE D-146 (2026-08-06): a v2, PROOF-BOUND registry. It used to be v1 with addresses spelled
+ // 0xA00+i, which no secp256k1 key can sign for, so this fixture described a fleet that could
+ // never satisfy AERE-PQC-REG-ARM-02 once that guard was wired. The registry is bound at H, the
+ // height this fixture arms the anchor from.
final KeccakDigest kd = new KeccakDigest(256);
final StringBuilder manifest = new StringBuilder();
manifest
@@ -177,7 +177,7 @@ public class PqCallerIntentTest {
.describedAs(
"HISTORY door at 1030, armed from 1000, no schedule: a node judging somebody else's "
+ "header cannot say which keys were in force there, so it REFUSES. Answering from "
- + "the head registry here is the rotation defect verbatim")
+ + "the head registry here is D2/T2 verbatim")
.isFalse();
assertThat(pqc.verifyAtOwnHead(OWN_HEAD, 0, MESSAGE, sealByIndexZero))
@@ -206,7 +206,7 @@ public class PqCallerIntentTest {
.describedAs(
"PqSealPersistenceTest restored 0 of 3 genuine seals under the first shape. A node "
+ "that cannot re-read its own seal file after a restart is a node that cannot "
- + "propose, and the file is the documented way out of that restart deadlock")
+ + "propose, and the file is the documented way out of the D-141 deadlock")
.isTrue();
assertThat(pqc.addressForIndexAtOwnHead(OWN_HEAD, 0))
.describedAs("and the index must bind, or every stored seal is dropped as unknown")
@@ -282,13 +282,13 @@ public class PqCallerIntentTest {
final FalconSealSupport pqc = FalconSealSupport.instance();
final PqRegistryHash.Registry held = PqRegistryHash.loadAuto(genesisPath);
final Map MEASURED FIRST, ON A NETWORK, NOT ASSUMED. A full activation rehearsal on a seven-node test
- * network found that with the anchor armed at K>0 a SIMULTANEOUS restart of every validator stops
- * the chain for good. The node said it verbatim: "refusing to
+ * MEASURED FIRST, ON A NETWORK, NOT ASSUMED. The full activation rehearsal on a seven-node test
+ * network (repetitie-activare-2026-08-05) found that with the anchor armed at K>0 a SIMULTANEOUS
+ * restart of every validator stops the chain for good. The node said it verbatim: "refusing to
* propose ... holds 0 valid eligible Falcon seal(s) ... threshold is 3", over "Attachment stays OFF
* (fail-safe)".
*
@@ -75,8 +75,8 @@ import org.junit.jupiter.api.io.TempDir;
* no-op rather than a second registry load.
* NOT MEASURED here, and named so it is not read as covered: that a real Besu process reads slot
@@ -84,9 +84,15 @@ import org.junit.jupiter.api.io.TempDir;
* rehearsal network is the instrument for it), and that seven live nodes recover from a real
* simultaneous restart with this binary. This class measures the decision the deadlock hinges on.
*/
+// The D-140 label is our internal finding id. It names a fact about this
+// code, not anything outside it.
public class PqFleetRestartArmingTest {
- /** Fleet size: seven, the validator count this deployment runs. */
+ /**
+ * Fleet size for THIS fixture. Not a statement about any live network: the 2026-08-05 decision
+ * to stay at seven was reversed, and the set has been nine since 2026-08-12. Seven is kept here
+ * because it is the size at which the margin arithmetic this class exercises is tightest.
+ */
private static final int N = 7;
/** Height at which the anchor contract is expected to be observable. */
@@ -110,6 +116,21 @@ public class PqFleetRestartArmingTest {
@BeforeEach
public void setUp() throws Exception {
+ // DATED 2026-08-20. This class never arms the certificate anchor, but FalconSealSupport's
+ // constructor consults it (anchorArmedFrom() -> PqAnchorProducer.config(), a per-JVM cache):
+ // a neighbouring test class that leaves an ARMED anchor config cached in this JVM turns every
+ // proof-less fixture below into an AERE-PQC-REG-ARM-02 refusal. Measured on the production
+ // tree that day: this class ALONE 5/5 green, inside the full suite the same 5 red, identical
+ // sources -- the 2026-08-11 order-luck lesson verbatim ("clearing the properties does not
+ // clear the caches"). The defence belongs to the consumer: start from an unarmed anchor,
+ // cache and properties both.
+ for (final String p : System.getProperties().stringPropertyNames()) {
+ if (p.startsWith("aere.pq.")) {
+ System.clearProperty(p);
+ }
+ }
+ org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.useConfigForTesting(
+ null);
resetFalconSingleton();
final SecureRandom rnd = SecureRandomProvider.createSecureRandom();
@@ -344,9 +365,9 @@ public class PqFleetRestartArmingTest {
* The seven-node rehearsal ran a GENESIS-anchored registry, and the line it logged after the
* simultaneous restart was the COVERAGE one: "no validator set has been observed yet, so registry
* COVERAGE cannot be proven. Attachment stays OFF (fail-safe)". That condition does not exist in
- * this tree: {@code grep} for it returns nothing, because the seal-attachment repair took the
- * fleet-wide question out of the per-commit gate. The rehearsal binary was built before that
- * repair landed, and still had it.
+ * this tree: {@code grep} for it returns nothing, because D-078 (2026-08-02) removed the fleet
+ * question from the per-commit gate. The rehearsal binary was built from the 2026-08-01 tree,
+ * which still had it.
*
* So this test states what is true HERE: a genesis-anchored node, freshly constructed, with no
* validator set observed and no block imported, IS armed. The rehearsal's measured deadlock is
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkArmingTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkArmingTest.java
index 2f76543..f7276f9 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkArmingTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkArmingTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -30,11 +30,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
- * FORK-HEIGHT ARMING. THE MEASUREMENT THAT DID NOT EXIST.
+ * D-079. THE MEASUREMENT THAT DID NOT EXIST.
*
- * The concern, as it was written down: a malformed forkBlock falls OPEN, with only a log line,
- * and arming it at or before the anchor observation height passes undetected. It stood as an
- * assertion with no command behind it for months. This file is the command that can fail.
+ * The registry entry reads: "a malformed forkBlock falls OPEN, with only a log line, and arming
+ * it at or before the anchor observation height passes undetected", and it carried {@code verifica:
+ * NICIUNA} since 18 July. This file is the command that can fail.
*
* Both halves of the finding are about the SAME shape of defect, the one the Holesky Pectra
* incident of February 2025 made expensive for everybody: a fork-activation parameter that is wrong
@@ -75,6 +75,8 @@ import org.junit.jupiter.api.io.TempDir;
* the guards that already abort, and nothing in this tree catches {@code
* FalconSealSupport.ActivationConfigException}.
*/
+// The D-079 label is our internal finding id. It names a fact about this
+// code, not anything outside it.
public class PqForkArmingTest {
/** Fleet size; nine, because the blocking guard refuses to arm below nine. */
@@ -92,8 +94,8 @@ public class PqForkArmingTest {
private static final String ANCHOR_ADDRESS = "0x0000000000000000000000000000000000000fa1";
/**
- * REGISTRY BINDING: the chain this fixture's registries are BOUND to. Every proof commits to it,
- * so it has to be stated rather than defaulted.
+ * AERE D-146: the chain this fixture's registries are BOUND to. Every proof commits to it, so it
+ * has to be stated rather than defaulted.
*/
private static final long CHAIN_ID = 2_800L;
@@ -104,7 +106,7 @@ public class PqForkArmingTest {
@BeforeEach
public void setUp() throws Exception {
- // REGISTRY BINDING: both registries below are v2 and PROOF-BOUND, bound at FORK, the
+ // AERE D-146 (2026-08-06): both registries below are v2 and PROOF-BOUND, bound at FORK, the
// height this fixture arms from. They used to carry addresses spelled 0xB00+i, which no
// secp256k1 key can sign for, so this whole fixture became unstartable the moment
// AERE-PQC-REG-ARM-02 was wired into the constructor.
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkThresholdReachabilityTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkThresholdReachabilityTest.java
index f9e4f36..81bdb8b 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkThresholdReachabilityTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkThresholdReachabilityTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -33,8 +33,7 @@ import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
import org.junit.jupiter.api.io.TempDir;
/**
- * THRESHOLD REACHABILITY, THE HALF THAT WAS STILL OPEN: is the threshold K one the fleet can be
- * GUARANTEED to meet?
+ * D-078, THE HALF THAT WAS STILL OPEN: is the threshold K one the fleet can be GUARANTEED to meet?
*
* The 2026-08-02 repair closed the mechanism that stopped the chain on one add-validator vote: it
* took the fleet-wide coverage question out of the per-commit attachment gate and made coverage a
@@ -67,8 +66,8 @@ import org.junit.jupiter.api.io.TempDir;
* is not re-anchored on the way there it produces a fleet that arms a threshold no proposer is
* guaranteed to meet. Before this guard a node in that state started, joined, armed, and the failure
* appeared later as a proposer that could not propose. That is the most expensive shape a
- * configuration error can take, and it is the same shape the address-binding guard already refused
- * to allow for a non-address-bound manifest.
+ * configuration error can take, and it is the same shape the A8 repair already refused to allow for
+ * a non-address-bound manifest.
*
* WHY AT CONFIG TIME AND NOWHERE ELSE. The lesson is borrowed, not invented: CometBFT applies a
* validator-set change only at H+2 and Ethereum's light-client protocol carries {@code
@@ -77,13 +76,14 @@ import org.junit.jupiter.api.io.TempDir;
* mechanism, because at seven nodes under one operator there is no committee to sample. We can copy
* the discipline: DECLARE the fleet size, compare it against the threshold at config time, and
* refuse to cross the boundary if the comparison fails. The same reasoning already produced
- * AERE-PQC-CFG-UNSAFE-04 and, for the fork height, AERE-PQC-CFG-UNSAFE-06/07 in the fork-arming
- * configuration guard.
+ * AERE-PQC-CFG-UNSAFE-04 and, for the fork height, AERE-PQC-CFG-UNSAFE-06/07 in D-079.
*
* NOT MEASURED here, and named so it is not read as covered: what a LIVE fleet does in the rounds
* between the vote landing and the first proposer failing. That needs a network. This class measures
* the decision, which is the thing a node can be stopped from taking.
*/
+// The D-078 label is our internal finding id. It names a fact about this
+// code, not anything outside it.
public class PqForkThresholdReachabilityTest {
/** Anchor activation height H. */
@@ -96,8 +96,8 @@ public class PqForkThresholdReachabilityTest {
private static final int K = 5;
/**
- * REGISTRY BINDING: the chain the registries this fixture writes are BOUND to. It is the same
- * value {@link #armAnchor} states in {@code aere.pq.chainId}: a registry bound to one chain and an
+ * AERE D-146: the chain the registries this fixture writes are BOUND to. It is the same value
+ * {@link #armAnchor} states in {@code aere.pq.chainId}: a registry bound to one chain and an
* anchor armed on another is a configuration this fixture must never accidentally describe.
*/
private static final long CHAIN_ID = 2_800L;
@@ -198,8 +198,8 @@ public class PqForkThresholdReachabilityTest {
@Test
public void withNoAnchorConfiguredTheGuardIsInert() throws Exception {
- // With aere.pq.anchorBlock unset there is no anchor, so K does not exist and there is nothing
- // to compare. A guard that could stop a node in that state would be a new way to lose the fleet,
+ // aere.pq.anchorBlock is UNSET on the live chain, so K does not exist and there is nothing to
+ // compare. A guard that could stop a node in that state would be a new way to lose the fleet,
// which is a strictly worse defect than the one it repairs.
writeAnchoredRegistry(7);
System.setProperty("aere.falcon.validatorCount", "9");
@@ -227,9 +227,8 @@ public class PqForkThresholdReachabilityTest {
@Test
public void aPositiveThresholdWithNoAnchoredKeysMustRefuseToStart() throws Exception {
// No manifest anywhere and K=5: guaranteed is 0, so every block at or above H would be rejected
- // for want of a certificate nobody can produce. Distinct from the genesis-binding refusal,
- // which only fires when aere.falcon.forkBlock is set; the anchor path has its own arming
- // height.
+ // for want of a certificate nobody can produce. Distinct from the A8 refusal, which only fires
+ // when aere.falcon.forkBlock is set; the anchor path has its own arming height.
System.setProperty("aere.falcon.validatorCount", "7");
armAnchor(K);
@@ -302,7 +301,7 @@ public class PqForkThresholdReachabilityTest {
* genesis is rather than by a flag.
*/
private void writeAnchoredRegistry(final int count) throws Exception {
- // REGISTRY BINDING: v2, proof-bound, bound at H, the height armAnchor() arms from. The
+ // AERE D-146 (2026-08-06): v2, proof-bound, bound at H, the height armAnchor() arms from. The
// rows come from PqV2Fixture because a v2 claim must be signed by the validator whose address
// is on the row, and the 0xA00+i addresses this used to spell have no key behind them.
final KeccakDigest kd = new KeccakDigest(256);
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkValidatorSetChangeTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkValidatorSetChangeTest.java
index 113c557..724650d 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkValidatorSetChangeTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkValidatorSetChangeTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -52,12 +52,12 @@ import org.junit.jupiter.api.io.TempDir;
import org.mockito.quality.Strictness;
/**
- * VALIDATOR-SET CHANGE UNDER AN ARMED ANCHOR. THE MEASUREMENT THAT DID NOT EXIST.
+ * D-078. THE MEASUREMENT THAT DID NOT EXIST.
*
- * The concern, as it was written down, was this: if PQC were armed, an ordinary add-validator
- * vote would stop the chain, because the Falcon blocking quorum follows the dynamic set and cannot
- * be reached inside the vote window. It was carried as UNMEASURED, on the argument that the direct
- * measurement would require ARMING PQC on a chain, which is exactly the thing that stops the chain.
+ * The registry entry reads: "if PQC were armed, an ordinary add-validator vote would stop the
+ * chain: the Falcon blocking quorum follows the dynamic set and cannot be reached inside the vote
+ * window", and it carried {@code verifica: NICIUNA} because "the direct measurement would require
+ * ARMING PQC on a chain, which is exactly the thing that stops the chain".
*
* That is true of a whole chain. It is NOT true of the decision that stops it. Every step from
* "the validator set changed" to "no block can be proposed" is taken by three objects in this
@@ -65,16 +65,16 @@ import org.mockito.quality.Strictness;
* decides whether this node emits a Falcon seal at all, {@link PqSealCache} holds what was heard,
* and {@link PqAnchorProducer#apply} decides whether this node may propose. This class drives those
* three with a REAL address-bound genesis-anchored registry and REAL Falcon-512 keys, and asks the
- * question that was held to be unaskable.
+ * question the registry says cannot be asked.
*
* WHAT EACH TEST MEASURES, and why each of them can fail:
*
* The three anchor patches plus the registry-binding arming gate are meant to travel onto the
- * seven live boxes BEFORE the activation height, so that the fleet is already running the binary
- * when the height arrives and activation is a restart-free event. That plan is only sound if a node
- * holding this binary and NO {@code aere.pq.*} configuration is indistinguishable from one holding
- * the binary it replaces: it must start, it must produce blocks, and it must not say a word about
- * an anchor that is not armed. If that property is lost, the whole package is unusable regardless
- * of how correct the anchor logic is, because it could not be staged.
+ * The three anchor patches plus the D-146 arming gate are meant to travel onto the seven live
+ * boxes BEFORE the activation height, so that the fleet is already running the binary when the
+ * height arrives and activation is a restart-free event. That plan is only sound if a node holding
+ * this binary and NO {@code aere.pq.*} configuration is indistinguishable from one holding the
+ * binary it replaces: it must start, it must produce blocks, and it must not say a word about an
+ * anchor that is not armed. If that property is lost, the whole package is unusable regardless of
+ * how correct the anchor logic is, because it could not be staged.
*
* WHY THE SILENCE IS MEASURED AND NOT ASSUMED. "It returns early, so it cannot log" is a reading
* of the code, not a measurement, and the integrated tree has four patches whose log sites nobody
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
index 0067f4b..393cbea 100644
--- 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
@@ -38,7 +38,7 @@ 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.
+ * D-228 (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
@@ -111,7 +111,7 @@ public class PqParentHeightAlignmentTest {
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.
+ // public-node shape D-228 was measured on.
// A genuine Falcon-512 signature by index 0 over MESSAGE.
final FalconSigner signer = new FalconSigner();
@@ -168,7 +168,7 @@ public class PqParentHeightAlignmentTest {
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 "
+ "D-228: 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));
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java
index d902552..dae5cb9 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java
@@ -47,8 +47,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
- * AERE REGISTRY BINDING. The registry says validator i has Falcon key k. Nothing said validator i
- * ever agreed to that, or that anybody holds k's secret.
+ * AERE D-146. The registry says validator i has Falcon key k. Nothing said validator i ever agreed
+ * to that, or that anybody holds k's secret.
*
* WHAT WAS MEASURED BEFORE THIS TEST EXISTED, on the real verification path, with the startup
* gate reporting MATCH and the header ACCEPTED every time:
@@ -68,7 +68,7 @@ import org.junit.jupiter.api.io.TempDir;
*
* KEYS. Every Falcon and ECDSA key here is generated in memory, used inside one test method, and
* never written anywhere but a JUnit temporary directory. Nothing in this file touches the key
- * ceremony or the controls that stand in front of real key generation.
+ * ceremony, the vault, or the three locks that stand in front of real key generation.
*/
class PqRegistryBindingTest {
@@ -172,7 +172,7 @@ class PqRegistryBindingTest {
// ceremony: the registry writer, who has every FALCON secret. It signs a perfectly valid
// possession proof for key 0 sitting under validator 1's address. Only the ECDSA claim, which
// needs validator 1's consensus key, stops it - and that is the whole argument for why a Falcon
- // proof-of-possession alone does not repair the attribution gap.
+ // proof-of-possession alone does not repair D-146.
final List WHAT THE REVIEW MEASURED. {@code PqSignerRegistry} had {@code addressForIndex(int)} and {@code
+ * WHAT THE DOSSIER MEASURED. {@code PqSignerRegistry} had {@code addressForIndex(int)} and {@code
* verify(int, Bytes, Bytes)} with no height, and {@code FalconSealSupport} held ONE registry loaded
* at start-up. So a header that passed both anchor rules was REJECTED the moment index 0's Falcon
* key was rotated - same header, same parent, same validator set.
*
- * WHAT WAS REPAIRED BEFORE THIS FILE, AND WHAT WAS NOT. The height-indexed registry change gave
- * the validation path {@code addressForIndexAt} / {@code verifyAt} and a height-indexed schedule.
- * The measurement of 2026-08-05 found the repair INERT, for a reason that is one line long: with no
+ * WHAT WAS REPAIRED BEFORE THIS FILE, AND WHAT WAS NOT. Commit f3ebe90c (D-081) gave the
+ * validation path {@code addressForIndexAt} / {@code verifyAt} and a height-indexed schedule. The
+ * measurement of 2026-08-05 found the repair INERT, for a reason that is one line long: with no
* {@code config.pqRegistryHash} in genesis - and there is none in any genesis this fleet runs -
* {@code keyAt} fell back to the registry in force AT THE HEAD, at every height. Height-aware
* signatures, head-registry answers. T2 stood exactly as measured.
@@ -95,8 +94,8 @@ public class PqRegistryHeightRefusalTest {
@BeforeEach
public void setUp() throws Exception {
- // AERE REGISTRY BINDING (2026-08-06): v2, proof-bound, bound at H. See PqV2Fixture for why the
- // addresses are derived from real secp256k1 keys and can no longer be spelled 0xA00+i.
+ // AERE D-146 (2026-08-06): v2, proof-bound, bound at H. See PqV2Fixture for why the addresses
+ // are derived from real secp256k1 keys and can no longer be spelled 0xA00+i.
final KeccakDigest kd = new KeccakDigest(256);
final StringBuilder manifest = new StringBuilder();
manifest
@@ -169,11 +168,10 @@ public class PqRegistryHeightRefusalTest {
// No schedule was ever loaded: verifyRegistryBindingOrAbort has not run, which is the state of
// every node on chain 2800 today, because config.pqRegistryHash is in no genesis this fleet
- // runs (measured 2026-08-05: a search across every deployment and monitoring configuration we
- // hold returns nothing).
+ // runs (measured 2026-08-05, grep over deploy/ and monitoring/ returns nothing).
assertThat(pqc.verifyAtHistoric(H, 0, MESSAGE, sealByIndexZero))
.describedAs(
- "T2: at the arming height itself, a node with no height-to-registry binding must "
+ "D2/T2: at the arming height itself, a node with no height-to-registry binding must "
+ "REFUSE. Before 2026-08-06 it answered from the registry in force at the HEAD, "
+ "so one key rotation made every block above H unverifiable while the node "
+ "reported success")
@@ -237,9 +235,9 @@ public class PqRegistryHeightRefusalTest {
// The first scheduled entry sits EXACTLY at the arming height, which is the rule the epoch-list
// design states: below H requiredHashAt is empty and the fallback is unreachable by anything
// that decides a header.
- // AERE REGISTRY BINDING (2026-08-06): the hash above is hashFor, not hashV1, because this
- // fixture's registry is now v2 and hashes under a different domain tag. A schedule entry that
- // names the v1 number names a registry this node does not hold.
+ // AERE D-146 (2026-08-06): the hash above is hashFor, not hashV1, because this fixture's
+ // registry is now v2 and hashes under a different domain tag. A schedule entry that names the
+ // v1 number names a registry this node does not hold.
final PqRegistryHash.Schedule schedule = scheduleFromGenesis(Map.of(H, hash));
pqc.verifyRegistryBindingOrAbort(0L, CHAIN_ID, schedule);
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java
index 7868c88..96debad 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -30,13 +30,12 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
- * SIGNER-REGISTRY ROTATION AND REVOCATION: the Falcon signer registry has no usable rotation and no
- * usable revocation.
+ * D-081: the Falcon signer registry has no usable rotation and no usable revocation.
*
* WHAT IS MEASURED HERE, and why it is measured against the real classes rather than described.
- * {@code config.pqRegistryHash} is a SCHEDULE of {block, hash} entries, and the format is documented
- * as one where "a later entry expresses a key rotation". This file asks whether that sentence
- * survives contact with the code that enforces it.
+ * {@code config.pqRegistryHash} is a SCHEDULE of {block, hash} entries, and the A8 dossier states
+ * that "a later entry expresses a key rotation". This file asks whether that sentence survives
+ * contact with the code that enforces it.
*
* The enforcement side is {@link PqRegistryHash#matchesAt} and, on the block path, {@code
* FalconSealSupport.registryBindingSatisfiedAt(height)}, which calls it. Both take exactly ONE
@@ -45,10 +44,12 @@ import org.junit.jupiter.api.io.TempDir;
* H2 there are two intervals with two different required hashes, and one file can satisfy at most
* one of them.
*
- * The consequence is not cosmetic and it is not confined to the rotation moment. The binding is
- * enforced while history is being acquired, not only at the head, so the whole range of heights has
- * to be satisfiable at once and not merely the current interval. That is the constraint the two
- * measurements below are written against.
+ * The consequence is not cosmetic and it is not confined to the rotation moment. {@code
+ * PqRegistryBindingRule} is a DETACHED rule, so it runs on the header-download path, and {@code
+ * PqAnchorSyncModeGuard} refuses to start an armed node in anything but FULL sync. A node acquiring
+ * history therefore validates every height, including the interval before the rotation. Holding the
+ * post-rotation registry it is refused there; holding the pre-rotation registry it is refused at the
+ * head. There is no third choice. ONE rotation makes the chain permanently unjoinable.
*
* This is the lesson Cosmos ADR-016 writes down explicitly: a rotation scheme has to keep the
* MAPPING FROM HEIGHT TO KEY SET, not only the current key set, or blocks signed under the old set
@@ -62,6 +63,8 @@ import org.junit.jupiter.api.io.TempDir;
* that the schedule really does express rotation and really does refuse a malformed one, so a
* failure of the two measurements cannot be blamed on the fixture.
*/
+// The D-081 label is our internal finding id. It names a fact about this
+// code, not anything outside it.
public class PqRegistryRotationTest {
private static final long CHAIN_ID = 2800L;
@@ -172,7 +175,7 @@ public class PqRegistryRotationTest {
} catch (final IOException e) {
throw new IllegalStateException(e);
}
- return PqRegistryHash.parseSchedule(node, "rotation fixture");
+ return PqRegistryHash.parseSchedule(node, "D-081 fixture");
}
/** Every height at which the binding is enforced and could differ across the rotation. */
@@ -202,7 +205,7 @@ public class PqRegistryRotationTest {
}
/**
- * THE REPAIR: the node holds the WHOLE scheduled history and resolves by height. This
+ * D-081 repair: the node holds the WHOLE scheduled history and resolves by height. This
* configuration did not exist before the repair, which is why the assertion below could not be
* satisfied by any node at all.
*/
@@ -263,7 +266,7 @@ public class PqRegistryRotationTest {
final String json =
"[{\"block\":" + H2 + ",\"hash\":\"0x" + h + "\"},{\"block\":" + H1 + ",\"hash\":\"0x" + h + "\"}]";
final JsonNode node = new ObjectMapper().readTree(json);
- assertThatThrownBy(() -> PqRegistryHash.parseSchedule(node, "rotation fixture"))
+ assertThatThrownBy(() -> PqRegistryHash.parseSchedule(node, "D-081 fixture"))
.isInstanceOf(PqRegistryHash.RegistryConfigException.class)
.hasMessageContaining("STRICTLY INCREASING");
}
@@ -303,10 +306,16 @@ public class PqRegistryRotationTest {
assertThat(complete)
.withFailMessage(
- "ROTATION UNUSABLE: one scheduled rotation at height %d leaves NO node configuration "
- + "that satisfies the registry binding at every enforced height. %s. A rotation "
- + "scheme must keep the whole HEIGHT-TO-KEY-SET mapping loadable, not only the "
- + "current entry.",
+ "ROTATION IS NOT USABLE: one scheduled rotation at height %d leaves NO node configuration "
+ + "that "
+ + "satisfies the registry binding at every enforced height. %s. A node that cannot "
+ + "satisfy the binding at a height cannot import a header at that height "
+ + "(PqRegistryBindingRule is DETACHED, so it runs on the header-download path), and "
+ + "PqAnchorSyncModeGuard forces FULL sync when the anchor is armed, so every node "
+ + "acquiring history must pass through the pre-rotation interval AND reach the head. "
+ + "Using the rotation mechanism once therefore makes the chain permanently "
+ + "unjoinable. A rotation scheme must keep the whole HEIGHT-TO-KEY-SET mapping "
+ + "loadable, not only the current entry.",
H2,
String.join("; ", report))
.isNotNull();
@@ -345,9 +354,10 @@ public class PqRegistryRotationTest {
assertThat(complete)
.withFailMessage(
- "REVOCATION UNUSABLE: revoking one compromised signer at height %d leaves NO node "
+ "REVOCATION IS NOT USABLE: revoking one signer at height %d leaves NO node "
+ "configuration that satisfies the binding at every enforced height. %s. The "
- + "revocation is expressible and is not usable.",
+ + "revocation is expressible and is not usable: performing it costs the ability to "
+ + "acquire the chain.",
H2,
String.join("; ", report))
.isNotNull();
@@ -419,8 +429,7 @@ public class PqRegistryRotationTest {
// Registry objects the test built itself. No operator can do that. What an operator can do is
// write a comma-separated list of FILE PATHS into aere.falcon.registry.history, and the node
// turns that string into the same set through parseRegistryPaths + loadAuto
- // (FalconSealSupport.verifyRegistryBindingOrAbort, the registry-binding block). If that route
- // were broken
+ // (FalconSealSupport.verifyRegistryBindingOrAbort, the D-081 block). If that route were broken
// the other six would still be green and the capability would still not be usable, which is the
// exact shape of "a green result in a reduced environment is true and worthless".
//
@@ -453,9 +462,10 @@ public class PqRegistryRotationTest {
}
assertThat(refused)
.withFailMessage(
- "ROTATION UNUSABLE on the route an operator can actually take: the history list %s "
- + "parses and loads, and the resulting set is still refused at %s. The library can "
- + "express the whole height-to-key-set mapping but the configuration string cannot reach "
+ "ROTATION IS NOT USABLE on the route an operator can actually take: the history list %s "
+ + "parses "
+ + "and loads, and the resulting set is still refused at %s. The library can express "
+ + "the whole height-to-key-set mapping but the configuration string cannot reach "
+ "it, so the rotation remains expressible and not usable.",
configured, refused)
.isEmpty();
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java
new file mode 100644
index 0000000..41e03e8
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java
@@ -0,0 +1,127 @@
+/* AERE crypto-agility, step 5 proofs. The D-147 control is the one that matters: the dangerous
+ * step hides at the END of the schedule, and the gate must walk all of it. */
+package org.hyperledger.besu.consensus.common.bft;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.security.SecureRandom;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.hyperledger.besu.crypto.SecureRandomProvider;
+import org.junit.jupiter.api.Test;
+
+class PqSchemeScheduleTest {
+
+ private static final String FALCON = "falcon-512";
+ private static final String SLHDSA = "slh-dsa-128s";
+
+ private final SecureRandom random = SecureRandomProvider.createSecureRandom();
+
+ // ------------------------------------------------------------------ parse + schemesAt
+
+ @Test
+ void schedulesParseAndAnswerByHeight() {
+ final PqSchemeSchedule orar =
+ PqSchemeSchedule.parse("100:falcon-512,200:falcon-512+slh-dsa-128s");
+ assertThat(orar.schemesAt(99)).isEmpty(); // inainte de prima treapta: v2 nearmat
+ assertThat(orar.schemesAt(100)).containsExactlyInAnyOrder(FALCON); // exact pe granita
+ assertThat(orar.schemesAt(150)).containsExactlyInAnyOrder(FALCON);
+ assertThat(orar.schemesAt(200)).containsExactlyInAnyOrder(FALCON, SLHDSA); // hibridul
+ assertThat(orar.schemesAt(1_000_000)).containsExactlyInAnyOrder(FALCON, SLHDSA);
+ }
+
+ // ------------------------------------------------------------------ refuzuri de parse
+
+ @Test
+ void unknownSchemeAnywhereRefusesTheWholeSchedule() {
+ assertThatThrownBy(() -> PqSchemeSchedule.parse("100:falcon-512,200:dilithium-notyet"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("dilithium-notyet");
+ }
+
+ @Test
+ void nonIncreasingHeightsRefuse() {
+ assertThatThrownBy(() -> PqSchemeSchedule.parse("200:falcon-512,100:falcon-512"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("strictly increase");
+ assertThatThrownBy(() -> PqSchemeSchedule.parse("200:falcon-512,200:slh-dsa-128s"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("strictly increase");
+ }
+
+ @Test
+ void emptyAndMalformedStepsRefuse() {
+ assertThatThrownBy(() -> PqSchemeSchedule.parse("")).isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> PqSchemeSchedule.parse("100")).isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> PqSchemeSchedule.parse("abc:falcon-512"))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> PqSchemeSchedule.parse("100:"))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> PqSchemeSchedule.parse("100:falcon-512+falcon-512"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("repeats");
+ assertThatThrownBy(() -> PqSchemeSchedule.parse("-5:falcon-512"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ // ------------------------------------------------------------- poarta de armare (D-147)
+
+ private HybridSignerRegistry registruCuAcoperire(final int falconi, final int slhuri) {
+ final Properties p = new Properties();
+ final int count = Math.max(falconi, Math.max(slhuri, 1));
+ p.setProperty("formatVersion", "hybrid-1");
+ p.setProperty("chainId", "2800");
+ p.setProperty("count", String.valueOf(count));
+ for (int i = 0; i < count; i++) {
+ p.setProperty(i + ".addr", "0x" + String.format("%040x", 0xB0 + i));
+ // fiecare index primeste macar o cheie; acoperirea per schema e controlata mai jos
+ if (i < falconi) {
+ p.setProperty(i + ".key." + FALCON,
+ Bytes.wrap(SealSchemes.FALCON_512.generate(random).publicRegistryForm()).toHexString());
+ }
+ if (i < slhuri) {
+ p.setProperty(i + ".key." + SLHDSA,
+ Bytes.wrap(SealSchemes.SLH_DSA_128S.generate(random).publicRegistryForm()).toHexString());
+ }
+ if (i >= falconi && i >= slhuri) {
+ p.setProperty(i + ".key." + FALCON,
+ Bytes.wrap(SealSchemes.FALCON_512.generate(random).publicRegistryForm()).toHexString());
+ }
+ }
+ return HybridSignerRegistry.fromProperties(p, "test");
+ }
+
+ @Test
+ void armabilityGateWalksTheWholeScheduleNotJustTheFirstStep() {
+ // the registry: 3 validators with Falcon, only 1 with SLH-DSA
+ final HybridSignerRegistry reg = registruCuAcoperire(3, 1);
+ // treapta PERICULOASA e ULTIMA: hibridul cere SLH-DSA cu acoperire 1 < K=3
+ final PqSchemeSchedule orar =
+ PqSchemeSchedule.parse("100:falcon-512,999999:falcon-512+slh-dsa-128s");
+ final var refusal = orar.firstUnsatisfied(reg, 3);
+ assertThat(refusal).isPresent();
+ assertThat(refusal.get()).contains("999999").contains(SLHDSA).contains("covers only 1");
+ }
+
+ @Test
+ void armabilityPassesWhenEverySchemeHasCoverage() {
+ final HybridSignerRegistry reg = registruCuAcoperire(3, 3);
+ final PqSchemeSchedule orar =
+ PqSchemeSchedule.parse("100:falcon-512,200:falcon-512+slh-dsa-128s");
+ assertThat(orar.firstUnsatisfied(reg, 3)).isEmpty();
+ // and the same gate's negative control: an impossible threshold must refuse
+ assertThat(orar.firstUnsatisfied(reg, 4)).isPresent();
+ }
+
+ @Test
+ void beforeTheFirstStepMeansLegacyNotSomeDefaultScheme() {
+ final PqSchemeSchedule orar = PqSchemeSchedule.parse("500:falcon-512");
+ assertThat(orar.schemesAt(0)).isEmpty();
+ assertThat(orar.schemesAt(499)).isEmpty();
+ assertThat(orar.steps()).hasSize(1);
+ assertThat(orar.steps().get(0).schemeIds()).isEqualTo(Set.of(FALCON));
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java
index 8967ff3..a9f4149 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright contributors to Besu / Aere Network.
+ * 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
@@ -56,15 +56,15 @@ import org.junit.jupiter.api.io.TempDir;
import org.mockito.quality.Strictness;
/**
- * THE SECOND HALF OF THE FLEET-RESTART CHAIN DEATH: the heard seals themselves.
+ * D-141. THE SECOND HALF OF THE FLEET-RESTART CHAIN DEATH: the heard seals themselves.
*
* MEASURED ON A NETWORK FIRST, NOT ASSUMED. With the anchor armed at K>0, a SIMULTANEOUS
- * restart of all seven validators kills the chain permanently, measured on an isolated rehearsal
- * network. The FIRST half of that deadlock was the registry, repaired the same day: it now
- * activates at start-up from chain-head state, and all seven nodes reported
- * "anchor activation at STARTUP from head state: SUCCEEDED". The chain died anyway. The refusal
- * only changed shape, from "registry address-bound=false" to "registry address-bound=TRUE ...
- * Heard 0 seal(s)", frozen 150 s then 298 s.
+ * restart of all seven validators kills the chain permanently (rehearsal
+ * repetitie-activare-2026-08-05, isolated chain 330858). The FIRST half of that deadlock was the
+ * registry, repaired the same day: it now activates at start-up from chain-head state, and all
+ * seven nodes reported "anchor activation at STARTUP from head state: SUCCEEDED". The chain died
+ * anyway. The refusal only changed shape, from "registry address-bound=false" to "registry
+ * address-bound=TRUE ... Heard 0 seal(s)", frozen 150 s then 298 s.
*
* THE SECOND CIRCLE. The Falcon seals over M(head) travel on nothing but the Commit messages of
* the head block, and those are never replayed after a restart. They exist nowhere else: the head's
@@ -100,6 +100,8 @@ import org.mockito.quality.Strictness;
* real simultaneous restart with a binary built from this tree. That needs the rehearsal network and
* is separate evidence. This class measures every decision that recovery depends on.
*/
+// The D-141 label is our internal finding id. It names a fact about this
+// code, not anything outside it.
public class PqSealPersistenceTest {
/** Anchor activation height H. */
@@ -111,7 +113,7 @@ public class PqSealPersistenceTest {
/** Height from which the staged threshold K is in force. */
private static final long K_AT = H + 10L;
- /** The threshold with full margin at N=7: K=3, so the margin equals f. */
+ /** The founder's decision of 2026-08-05: N=7 stays, and K=3 is the value with full margin. */
private static final int K = 3;
private static final int N = 7;
@@ -136,7 +138,7 @@ public class PqSealPersistenceTest {
public void setUp() throws Exception {
dataDirectory = Files.createDirectories(tmp.resolve("besu-data"));
- // REGISTRY BINDING: v2, proof-bound, bound at H. See PqV2Fixture.
+ // AERE D-146 (2026-08-06): v2, proof-bound, bound at H. See PqV2Fixture.
final KeccakDigest kd = new KeccakDigest(256);
final StringBuilder manifest =
new StringBuilder("{\"config\":{\"aereFalconRegistry\":{")
@@ -257,8 +259,8 @@ public class PqSealPersistenceTest {
/**
* Persisting seals is only defensible because a seal is SELF-AUTHENTICATING: it is re-verified at
* read, against the anchored registry, over M rebuilt from the head this process just loaded. If
- * that were not so, the file would be exactly the unbound-registry defect in another coat -
- * state believed because it sits in a file a node can be pointed at.
+ * that were not so, the file would be exactly defect A8 in another coat - state believed because
+ * it sits in a file a node can be pointed at.
*
* Three shapes of forgery are in the one file, because "a forged seal" is not one thing:
*
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java
index fc1333f..8122465 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java
@@ -36,7 +36,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
- * AERE SIGNED HEIGHT. THE SILENT DEFERRAL, and it is the worst of the three because nothing shows
+ * AERE D-B (2026-08-06). THE SILENT DEFERRAL, and it is the worst of the three because nothing shows
* it.
*
* WHAT IS SUPPOSED TO BE TRUE. Every row of a v2 registry carries two signatures - a Falcon
@@ -46,8 +46,7 @@ import org.junit.jupiter.api.io.TempDir;
*
* WHAT WAS ACTUALLY TRUE UNTIL THIS FILE. {@code bindHeight} was never compared with the {@code
* block} of the schedule entry that puts the registry in force. Not anywhere. The two numbers had
- * been in the same lexical scope ever since the schedule became height-indexed, and were never put
- * on the same expression.
+ * been in the same lexical scope since D-081 and were never put on the same expression.
*
* WHY THE HASH DOES NOT CATCH IT, which is the part that makes this invisible rather than merely
* missing. {@code bindHeight} is INSIDE the v2 pre-image, so it is covered by the hash - and that is
@@ -297,7 +296,7 @@ public class PqSignedHeightTest {
// The honest limitation has to be IN the message, or an operator will read this as a
// consensus guarantee it is not.
.hasMessageContaining("DETECTION on this node only")
- .hasMessageContaining("all seven nodes and in the same change");
+ .hasMessageContaining("on every node and in the same change");
}
@Test
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java
index 705838d..4826779 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java
@@ -33,7 +33,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
- * AERE ROTATION HISTORY AT STARTUP. AFTER THE FIRST ROTATION, NO NODE COULD BE RESTARTED WITH ITS OWN CORRECT
+ * AERE D-A (2026-08-06). AFTER THE FIRST ROTATION, NO NODE COULD BE RESTARTED WITH ITS OWN CORRECT
* CONFIGURATION.
*
* WHAT WAS MEASURED, and it was measured twice: once during the rotation rehearsal on a network
@@ -45,9 +45,8 @@ import org.junit.jupiter.api.io.TempDir;
* THE DEFECT WAS THE ORDER OF TWO BLOCKS OF CODE. {@code
* FalconSealSupport.verifyRegistryBindingOrAbort} loaded ONE registry, the primary, and handed it to
* the guard. The history list was read FORTY-ONE LINES FURTHER DOWN, to build the height-resolved
- * set the height-indexed registry change introduced. So the refusal was thrown before the code that
- * knew the answer had run. The guard was not wrong about what it compared; it was never shown the
- * other files.
+ * set D-081 introduced. So the refusal was thrown before the code that knew the answer had run. The
+ * guard was not wrong about what it compared; it was never shown the other files.
*
* WHY IT BITES EXACTLY AFTER A ROTATION AND NEVER BEFORE. The primary registry is the genesis
* manifest, and genesis does not change. A rotation adds a SECOND entry to {@code
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqV2Fixture.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqV2Fixture.java
index 2c7108f..4fb9529 100644
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqV2Fixture.java
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqV2Fixture.java
@@ -37,13 +37,13 @@ import org.bouncycastle.pqc.crypto.falcon.FalconPublicKeyParameters;
import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
/**
- * AERE REGISTRY BINDING (2026-08-06). The shared probe fleet every arming fixture is now built
- * from, and the reason it had to exist.
+ * AERE D-146 (2026-08-06). The shared probe fleet every arming fixture is now built from, and the
+ * reason it had to exist.
*
* WHAT IT REPLACED, and why the replacement is not cosmetic. Until 2026-08-06 seven separate
* fixtures built their registries around addresses spelled {@code String.format("0x%040x", 0xA00 +
* i)}. Those addresses are arithmetic, not keys: no secp256k1 private key produces them, so no
- * validator can ever sign a binding claim for one. The moment {@code AERE-PQC-REG-ARM-02} was wired
+ * validator can ever sign a D-146 claim for one. The moment {@code AERE-PQC-REG-ARM-02} was wired
* into {@code FalconSealSupport}, all seven fixtures described a fleet that CANNOT EXIST - armed,
* and provably unable to produce the registry the arming path now requires. Measured on 2026-08-06:
* 35 tests across 7 classes, every failure carrying AERE-PQC-REG-ARM-02.
@@ -215,9 +215,8 @@ public final class PqV2Fixture {
/**
* The bytes the genesis anchor slot commits to for row {@code i}: {@code address || publicKey},
- * which is what {@code hashV0Legacy} accumulates. Unchanged by the v2 binding work - the proofs
- * are outside the legacy pre-image - and kept here so a fixture cannot drift from the row it just
- * wrote.
+ * which is what {@code hashV0Legacy} accumulates. Unchanged by D-146 - the proofs are outside the
+ * legacy pre-image - and kept here so a fixture cannot drift from the row it just wrote.
*
* @param i the row index
* @return the anchored pre-image bytes for that row
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PreparePqAttachGateTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PreparePqAttachGateTest.java
new file mode 100644
index 0000000..5168978
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PreparePqAttachGateTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright contributors to Besu.
+ *
+ * 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 static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.tuweni.bytes.Bytes32;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * THE GATE that emits a seal on PREPARE (step 2 of the 2026-08-28 design note).
+ *
+ * What is measured here is the CONFIGURATION SURFACE, which is exactly the part that gets typed
+ * by hand and therefore mistyped: absent means never, a good value means from that height onwards,
+ * and a MISTYPED value refuses loudly instead of booting the node disarmed. The lesson paid for in
+ * the anchor loader is that a stray character must never disarm silently, because then nobody finds
+ * out.
+ *
+ * What is NOT measured here, and it is said plainly: that an ARMED node actually produces a
+ * seal. That needs a Falcon key and a registry bound to addresses, which means a network; it is
+ * measured at the coverage step, on a testnet. What is proven here is that the gate is closed by
+ * default and cannot be opened by accident.
+ */
+class PreparePqAttachGateTest {
+
+ @AfterEach
+ void clearTheProperty() {
+ System.clearProperty(FalconSealSupport.PREPARE_ATTACH_PROPERTY);
+ }
+
+ @Test
+ void withoutThePropertyTheGateIsClosedForever() {
+ assertThat(FalconSealSupport.prepareAttachBlock()).isEqualTo(Long.MAX_VALUE);
+ }
+
+ @Test
+ void aGoodValueIsReadAsGiven() {
+ System.setProperty(FalconSealSupport.PREPARE_ATTACH_PROPERTY, "16500000");
+ assertThat(FalconSealSupport.prepareAttachBlock()).isEqualTo(16_500_000L);
+ }
+
+ @Test
+ void zeroIsALEGALValue() {
+ // A threshold of zero means "from genesis", and that is a legitimate configuration on a
+ // testnet. Treated as "unset", a correctly configured testnet would run disarmed in silence.
+ System.setProperty(FalconSealSupport.PREPARE_ATTACH_PROPERTY, "0");
+ assertThat(FalconSealSupport.prepareAttachBlock()).isZero();
+ }
+
+ @Test
+ void aMISTYPEDValueRefusesLoudly() {
+ for (final String bad : new String[] {"nu-e-numar", "16_500_000", "1e6", "-1", " "}) {
+ System.setProperty(FalconSealSupport.PREPARE_ATTACH_PROPERTY, bad);
+ if (bad.isBlank()) {
+ // whitespace is "unset", not a mistyped value: an empty field in a configuration file
+ // must not stop a node
+ assertThat(FalconSealSupport.prepareAttachBlock()).isEqualTo(Long.MAX_VALUE);
+ continue;
+ }
+ assertThatThrownBy(FalconSealSupport::prepareAttachBlock)
+ .as("the value '%s'", bad)
+ .isInstanceOf(FalconSealSupport.ActivationConfigException.class)
+ .hasMessageContaining("AERE-PQC-PREPARE-CONF-01");
+ }
+ }
+
+ @Test
+ void withNoKeyNothingIsSignedEvenWithTheGateOpen() {
+ // The gate is open from genesis and still nothing comes out: the node has no Falcon key. That
+ // is precisely the condition that makes the binary safe to roll onto the fleet before any
+ // decision is taken.
+ System.setProperty(FalconSealSupport.PREPARE_ATTACH_PROPERTY, "0");
+ assertThat(FalconSealSupport.instance().signPrepare(1L, Bytes32.ZERO)).isEmpty();
+ }
+
+ @Test
+ void thePREPAREGateIsNotTheCOMMITGate() {
+ // If it were the same one, rolling the binary onto the fleet would become a flag day: PREPARE
+ // emission would start the moment commit emission does, and that one is already on since block
+ // 13,889,296 on chain 2800.
+ assertThat(FalconSealSupport.PREPARE_ATTACH_PROPERTY).isNotEqualTo("aere.falcon.attachBlock");
+ System.setProperty(FalconSealSupport.PREPARE_ATTACH_PROPERTY, "16500000");
+ assertThat(FalconSealSupport.prepareAttachBlock()).isEqualTo(16_500_000L);
+ // the commit property stays untouched by the PREPARE one
+ assertThat(System.getProperty("aere.falcon.attachBlock")).isNull();
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java
new file mode 100644
index 0000000..91ca9bb
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java
@@ -0,0 +1,145 @@
+/* AERE crypto-agility, step 1 proofs. Every green here has a red twin: flipped signatures,
+ * flipped messages, wrong keys, and the cross-scheme controls that are the whole point of the
+ * layer (a Falcon artefact must never verify as SLH-DSA, and vice versa). A layer whose schemes
+ * cannot be told apart would be worse than no layer. */
+package org.hyperledger.besu.consensus.common.bft;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Optional;
+
+import org.hyperledger.besu.crypto.SecureRandomProvider;
+import org.junit.jupiter.api.Test;
+
+class SealSchemeAgilityTest {
+
+ private static final byte[] MESSAGE = "aere anchor commit hash stand-in".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] OTHER_MESSAGE = "a different message entirely....".getBytes(StandardCharsets.UTF_8);
+
+ private final SecureRandom random = SecureRandomProvider.createSecureRandom();
+
+ // ------------------------------------------------------------------ per-scheme sign/verify
+
+ @Test
+ void falconSignsAndVerifies() {
+ roundTrip(SealSchemes.FALCON_512);
+ }
+
+ @Test
+ void slhDsaSignsAndVerifies() {
+ roundTrip(SealSchemes.SLH_DSA_128S);
+ }
+
+ private void roundTrip(final SealScheme scheme) {
+ final SealScheme.GeneratedPair pair = scheme.generate(random);
+ final Optional Sigiliul vine GATA CALCULAT de la apelant, si asta nu e comoditate: semnaturile Falcon sunt
+ * randomized, so signing the same message twice yields two different byte strings. If the local
+ * copy and the one on the wire each signed their own, the same validator would produce two
+ * valide si DIFERITE pentru aceeasi runda. Se calculeaza o data, sus, si se trece prin amandoua.
+ *
+ * @param roundIdentifier the round identifier
+ * @param digest the digest
+ * @param falconSeal the seal, or empty
+ */
+ public void multicastPrepare(
+ final ConsensusRoundIdentifier roundIdentifier,
+ final Hash digest,
+ final Optional AERE HIBRID (2026-08-25), the founder's step two of 2026-08-07. A hybrid certificate is
+ * Falcon-512 PLUS a second, structurally unrelated scheme (SLH-DSA/SPHINCS+): if lattices fall
+ * the hash-based one holds, and the reverse. Falcon stays in the legacy slot and the OTHER
+ * schemes travel here, so:
+ *
+ * The extras are encoded with {@link PqAnchorV2}, the same scheme-tagged codec the V2 anchor
+ * certificate uses: one vocabulary, one canonicality discipline, one place to get it wrong.
+ *
+ * ADDING THIS ELEMENT IS A CONSENSUS BREAKING CHANGE, exactly as {@link #readFrom(RLPInput)}
+ * warns: an older binary cannot parse a commit that carries it. What protects the fleet is not
+ * leniency, which cannot work, but the EMISSION gate: nothing emits extras until every peer can
+ * read them. Same discipline as the Falcon attachment gate.
+ *
+ * @param roundIdentifier the round identifier
+ * @param digest the digest
+ * @param commitSeal the ECDSA commit seal (decisive)
+ * @param falconSeal the Falcon seal; REQUIRED whenever extras are present
+ * @param extraSeals the non-Falcon scheme seals; empty for every commit on the fleet today
+ */
+ public CommitPayload(
+ final ConsensusRoundIdentifier roundIdentifier,
+ final Hash digest,
+ final SECPSignature commitSeal,
+ final Optional AERE PQ (2026-08-28), pasul 1: firul poate purta sigiliul, si nimic nu il emite inca -
+ * fiecare apel de azi trece prin varianta fara sigiliu de mai sus. Ca la commit, semnatura ECDSA
+ * a autorului acopera INTREG payload-ul, deci si sigiliul, ceea ce leaga indexul revendicat de
+ * identitatea celui care trimite mesajul.
+ *
+ * @param roundIdentifier the round identifier
+ * @param digest the digest
+ * @param falconSeal the author's post-quantum seal, or empty
+ * @return the prepare
+ */
+ public Prepare createPrepare(
+ final ConsensusRoundIdentifier roundIdentifier,
+ final Hash digest,
+ final Optional AERE PQ (2026-08-28), step 1 of PREPARE-SI-ROUNDCHANGE-SUB-PQ-PROIECTARE-2026-08-28: a PREPARE
+ * MAY carry an OPTIONAL Falcon-512 seal from its author, appended at the end, exactly as
+ * {@code CommitPayload} does. A PREPARE without a seal encodes byte for byte as upstream,
+ * and that is precisely the property that lets the binary be rolled onto a live fleet without a
+ * flag day.
+ *
+ * NOTHING EMITS SUCH A PREPARE YET. This file only makes the wire capable of carrying one
+ * and of refusing a malformed one. Emission is the next step and has its own gate, following the
+ * rule paid for at commit: first the binary everywhere, then emission, and only much later
+ * enforcement.
+ *
+ * What the seal signs is NOT this file's business, and the design note states it: its own
+ * domain {@code AERE-PQ-PREPARE-1} over (chainId, number, ROUND, digest). If it signed the same
+ * bytes as a commit seal, a PREPARE seal given honestly could be pasted onto a forged COMMIT and
+ * the enforcement there would accept it.
+ */
+public class PreparePayload extends QbftPayload {
+ private static final int TYPE = QbftV1.PREPARE;
+ private final ConsensusRoundIdentifier roundIdentifier;
+ private final Hash digest;
+ private final Optional STRICTLY CANONICAL, as in {@code CommitPayload} and for the same reason: a PREPARE is an
+ * AUTHENTICATED message, and the author is recovered from the RE-ENCODED payload, not from the
+ * bytes that arrived. Anything the decoder tolerated silently would give several byte strings that
+ * authenticate to the same validator - that is malleability. Decode, re-encode, and the result
+ * must be exactly what came in.
+ *
+ * @param rlpInput the rlp input
+ * @return the prepare payload
+ * @throws RLPException if the received bytes are not the payload's unique canonical encoding
+ */
+ public static PreparePayload readFrom(final RLPInput rlpInput) {
+ final RLPInput payloadRlp = rlpInput.readAsRlp();
+ final Bytes received = payloadRlp.raw();
+
+ payloadRlp.enterList();
+ final ConsensusRoundIdentifier roundIdentifier = readConsensusRound(payloadRlp);
+ final Hash digest = Payload.readDigest(payloadRlp);
+
+ // AERE PQ: the OPTIONAL seal [index, signature]. A PREPARE without one ends the list here and
+ // decodes to Optional.empty(), so it stays identical to upstream.
+ Optional the type parameter of BftMessage
+ * @param message the message
+ * @param bftMessage the bft message
+ * @param handleMessage the handle message
+ * @param isReplayed the message is being replayed
+ */
+ protected > void consumeMessage(
+ final QbftMessage message,
+ final P bftMessage,
+ final Consumer handleMessage,
+ final boolean isReplayed) {
+ LOG.trace("Received BFT {} message", bftMessage.getClass().getSimpleName());
+
+ // Discard all messages which target the BLOCKCHAIN height (which SHOULD be 1 less than
+ // the currentHeightManager, but CAN be the same directly following import).
+ if (bftMessage.getRoundIdentifier().getSequenceNumber()
+ <= blockchain.getChainHeadBlockNumber()) {
+ // AERE D-227: before the message dies here, keep its Falcon seal if it is still useful.
+ pqSalvageLateSeal(bftMessage);
+ LOG.debug(
+ "Discarding a message which targets a height {} not above current chain height {}.",
+ bftMessage.getRoundIdentifier().getSequenceNumber(),
+ blockchain.getChainHeadBlockNumber());
+ return;
+ }
+
+ if (processMessage(bftMessage, message)) {
+ gossiper.send(message, isReplayed);
+ handleMessage.accept(bftMessage);
+ }
+ }
+
+ /**
+ * AERE D-227 (2026-08-14): keep the Falcon seal of a Commit that arrives AFTER its block was
+ * imported, instead of discarding it with the message.
+ *
+ * Why this exists, measured on chain 2800: a block imports on the quorum-th Commit, and the
+ * Commits of the slowest validators consistently arrive tens of milliseconds later - after the
+ * height gate above starts discarding them. Their Falcon seals never reached the seal cache, so
+ * the proposer of the NEXT block (which reads the cache roughly half a block-period later, plenty
+ * of time) could never carry them. Seal circulation measured per signer: the two slowest-disk
+ * nodes appeared in 3% and 14% of other proposers' certificates while appearing in 100% of their
+ * own. The ECDSA path is unaffected either way - by the time a Commit reaches this branch its
+ * block is already imported.
+ *
+ * What is deliberately NOT relaxed: the message itself still dies. Only the seal is copied
+ * out, and only when ALL of the following hold: the message is a Commit carrying a seal, its
+ * height is EXACTLY the chain head (an older seal can never be asked for again), its digest is
+ * the head's own hash (a losing round or a fork sibling is not ours to keep), and its author is
+ * a known validator (so a non-validator peer cannot write into the cache). A seal that lies
+ * about its signer index still cannot reach a header: the producer Falcon-verifies every cached
+ * seal against the anchored registry before carrying it.
+ */
+ private void pqSalvageLateSeal(final BftMessage> bftMessage) {
+ if (!(bftMessage instanceof Commit commit)) {
+ return;
+ }
+ final Optional Mesajul semnat are DOMENIUL LUI si contine RUNDA - vezi PqAnchor.prepareMessage si
+ * proiectarea din 2026-08-28. Cu domeniul commitului, un sigiliu de PREPARE dat cinstit ar putea
+ * fi lipit pe un COMMIT falsificat.
+ */
+ private Optional
+ *
+ */
+ static String resolve(final String sysProp, final String envVar) {
+ final String neutralProp =
+ sysProp.startsWith("aere.falcon.")
+ ? "aere.pq.sig." + sysProp.substring("aere.falcon.".length())
+ : null;
+ final String neutralEnv =
+ envVar.startsWith("AERE_FALCON_")
+ ? "AERE_PQ_SIG_" + envVar.substring("AERE_FALCON_".length())
+ : null;
+ final String neutral = firstNonBlank(
+ neutralProp == null ? null : System.getProperty(neutralProp),
+ neutralEnv == null ? null : System.getenv(neutralEnv));
+ final String legacy = firstNonBlank(System.getProperty(sysProp), System.getenv(envVar));
+ if (neutral != null && legacy != null && !neutral.trim().equals(legacy.trim())) {
+ throw new ActivationConfigException(
+ ActivationConfigException.Kind.SYNTAX,
+ "AERE-PQC-CFG-DUAL-NAME-01",
+ "AERE PQC: "
+ + neutralProp
+ + " and its legacy twin "
+ + sysProp
+ + " are BOTH set, to DIFFERENT values ('"
+ + neutral
+ + "' vs '"
+ + legacy
+ + "'). Refusing to start: a node that silently prefers one spelling turns a typo "
+ + "into a consensus divergence. Set exactly one, or both to the same value.");
}
- final String e = System.getenv(envVar);
- if (e != null && !e.isBlank()) {
- return e;
+ return neutral != null ? neutral : legacy;
+ }
+
+ private static String firstNonBlank(final String a, final String b) {
+ if (a != null && !a.isBlank()) {
+ return a;
+ }
+ if (b != null && !b.isBlank()) {
+ return b;
}
return null;
}
@@ -2529,12 +2577,12 @@ public final class FalconSealSupport {
*
+ *
+ *
+ *
*
*
* The defect, measured on the real code path on 2026-08-06
*
* What this does NOT defend against, stated plainly
*
*
- *
@@ -407,7 +406,7 @@ public final class PqRegistryHash {
* outside it, they would be advisory: a node could be handed the same registry with the proof
* fields deleted, it would hash the same, satisfy the schedule, and load without ever verifying
* anything. With them inside, stripping a proof is a different registry with a different hash and
- * the existing genesis-binding guard refuses it. That is also why v2 has its OWN domain tag: a v1 file cannot
+ * the existing A8 guard refuses it. That is also why v2 has its OWN domain tag: a v1 file cannot
* collide with a v2 schedule entry, so a format downgrade is refused by machinery that already
* exists rather than by a new rule that could be forgotten.
*
@@ -436,7 +435,7 @@ public final class PqRegistryHash {
}
/**
- * ROW BINDING. keccak256 of the canonical v2 pre-image, as 64-hex with no {@code 0x}.
+ * D-146. keccak256 of the canonical v2 pre-image, as 64-hex with no {@code 0x}.
*
* @param registry the loaded registry, which must be proof-bound
* @param chainId the chain id this registry is bound to
@@ -447,7 +446,7 @@ public final class PqRegistryHash {
}
/**
- * ROW BINDING. The canonical hash OF THIS REGISTRY: v2 when it carries binding proofs, v1 when it does
+ * D-146. The canonical hash OF THIS REGISTRY: v2 when it carries binding proofs, v1 when it does
* not. Every comparison against a schedule entry goes through here, so a proof-bound registry is
* compared as v2 everywhere and a legacy one keeps exactly the number it had before this change.
*
@@ -460,13 +459,13 @@ public final class PqRegistryHash {
}
/**
- * ROW BINDING. The ARMING precondition: refuse to arm the anchor over a registry whose rows are not
+ * D-146. The ARMING precondition: refuse to arm the anchor over a registry whose rows are not
* bound to their validator addresses by signatures.
*
*
* java -cp 'besu/lib/*' org.hyperledger.besu.consensus.common.bft.PqRegistryHashTool \
- * verify --chain-id 2800 --genesis <config-dir>/genesis.json \
- * --history <config-dir>/falcon/registry-epoca-0.properties
+ * verify --chain-id 2800 --genesis ./genesis-2800.json \
+ * --history ./falcon/registry-epoch-0.properties
*
*
*
+ *
+ * "What coverage genuinely protects - that blocking is not ARMED over a partial manifest - is an
+ * arm-time decision, and it is made at arm time by armingReadinessDiagnostic() and by the operator".
+ *
+ *
+ *
+ *
+ * N=7, keyed 7, quorum 5 -> 5 guaranteed K=5 reachable, margin exactly 0
+ * N=9, keyed 7, quorum 6 -> 4 guaranteed K=5 NOT guaranteed
+ *
+ *
+ *
+ *
+ *
+ *
+ * N=7, keyed 7, quorum 5 -> 5 guaranteed K=5 is met, with EXACTLY zero margin
+ * N=8, keyed 7, quorum 6 -> 5 guaranteed K=5 is still met, still zero margin
+ * N=9, keyed 7, quorum 6 -> 4 guaranteed K=5 is NOT guaranteed any more
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ @Test
+ public void aForgedSealInTheFileIsRejectedAtReadAndNeverEntersTheCache() throws Exception {
+ final List
*
+ *
+ *
+ *