diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt
index 6d8b135..8d01298 100644
--- a/anchor/MANIFEST-sha256.txt
+++ b/anchor/MANIFEST-sha256.txt
@@ -5,12 +5,14 @@ computed them, and the same 276 files once produced two different aggregates her
329e4e2f93143cc8af092f0a951fa23619c12c4ec1793b053654f8e26e088da0 app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java
-1615ecaf19f75af3e91c27c029177bf0faf51839f5589878b8c3562371299865 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
+0ce3fcfdcfa7ab17f4fd1251e9066e3e1c03a59047a01fb548f490391fe4cdb4 app/src/main/java/org/hyperledger/besu/controller/PqTipAdoptionGate.java
+505066636bdd2a0e6908d490ad0b3341788d270320aed81dc2923572d61216a7 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
68d635954ef33448a661256f50642eeeff2f7bb5874a54ffc2e64193c83fe391 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelay.java
ed2e46aaf2e74661ad831dc31df0b6f795f28f4a94496a7d226feedf2a770fde consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java
+a6c2bbf05eab4173a749c67ef8b7bce6d184d8eac1b445acb847efd082dc6d62 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTags.java
290d94bb50f23666b131ec19659982c15f1e6cbaf8f8bfe128df9acb2f2f79fd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqHeaderSeal.java
348135b2e4d0a6a8ce91489c94fbfa77da2536196c13823676b33ea9d08f8fb9 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqIdentity.java
c531f9e719c48d3d5da2800fdef4506bd167af4dad4e25af96b4353a4390067d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java
@@ -31,7 +33,7 @@ e16b2c654fe21893c7972dac71f3ffafd1e4e8efbecdb6f95051820e36fbd48e consensus/comm
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
+660ba13ad60c49e86cf9ee4ced2b6932a5422d988f85c5c23098a92b029f4d25 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java
11f1c2a97a40df999471977cb3eab9af4e816081b100e345e8c962ef3bbf76f5 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryV3.java
2ee139d21f73ff3417f6ad61b233e5ca3a95f92e68a4284eb99a47b1f8f4b58b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSchemeSchedule.java
c0c93a096f4eceab6bf92cc3618ce7cee1e54d2d20b6ac781850a390ec6e514f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java
@@ -44,39 +46,41 @@ addba40c0d931a3ecfa3b2f0179311dacaa604884e3ebb1d79f958bd913f72e2 consensus/comm
444961503d515cb9f045729577e190941e59baa4bd90438a2da05f41fed7750a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SlhDsaSealScheme.java
454eeca487505e70c7f0000f5401b6bf4530da1d5ce6bcb803427f1d08e3cb1c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java
e89deb6f346c221affa717eafa464d497401b7b5a01c84c144dace42032879a9 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java
-baa7f138046c501c5baecae3615d54ed0bbd6fea47d7f84a3d71eaa5698d182d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
-7c464b927fcb39ab017c543ca42bcd076b1dc69573c7ddefda1e685ebf9eb9ba consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
-ddb17d7c6e1b762a9e8bcc924a17d42218cb5ec16528d80170bf2ff07e40aba2 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
-daf4c242682df6365aa7459990a9ba7b5f97e9a06cfff95c96afe1eadc061647 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/IndexedDigest.java
-ddd60bf21ee7207b5b7d72fda5cd8bd9f9515536744379aa7d1bb7e3cdc9abdb consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/JdkDigest.java
-108a2d252eea02e005889942966d2dcf845e1d01fc5e7bfdf85b58e1ac0697cd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/NodeEntry.java
-91ff4c4bc681bde06e72188e28177170645ca72cb72a08b08c3cf2283376a933 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/PK.java
-ef1fcba8ac1d98f6bcb44616b33551cc37c21fa4fd0e5adc967bd4960c6bc184 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG.java
-9b9b7b171e816d6b142978ada7ea4b4491dbad025d26f431d6a6fc03130b047a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_FORS.java
-21371b535b9818ba0bb4b094837b11b00dc3132187a7b67f1b11196ab386f1f5 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_XMSS.java
-07ed38417f5f4a4c56631c82651e70708ab47b8b6f017096ea281f0436180671 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SK.java
-8107ac08eabfee9016ecea9cdb53b78409f322c49706224def9c05501b297985 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngine.java
-efa6edc2f6cdf5e67074ef793fb15fff1bf41f5030476b7f8de991ada54b15eb consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngineProvider.java
-fa89107a6ff29b8d55d66949dd810cad36851a8ac847a0f7f33a843551f0e359 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyGenerationParameters.java
-48e4e7448b8076376bcd5720e20dd36d07b9d7a953fdf992691d54fa20f1709d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyPairGenerator.java
-14411ba911129fb56f95c62a26aa00de8b2fa7de5994563ec10d89ada95cc697 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyParameters.java
-c72f197a0f8967aec5e08d1c4bb30030b9dc16c27711567ca40ad0cbe845c989 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAParameters.java
-3c39912f4a7f1d56ffbaf1b249ecb6726cbf98180e93c6a924fe3318552caaf0 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPrivateKeyParameters.java
-3a18d7af3c85ab6a9239ff52be7524d141ed7e9e6cf84d34e8e4957434596b4c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPublicKeyParameters.java
-636399666ed9490b29a4cb85d2b3cddf2ef4e8f8fa9a8ecf2315a8abf3e2e6de consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.java
-bd6833075646ffa2f4fca2761e3d68ccbc37f7c5bb209ae2e40f8cfd469dce14 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java
+1e0113154d79a91a07104bcc67a9a843847751b921c5f9b7cc96d7bc30a1452f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
+15ab42221f35aeb0dfd468bcb840254f08ccfccb5ff36fba559ac1017beca5ee consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
+51b7d889188793a307bccb297d983f9318548e6066ad6228e37170585eda665f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
+b4270fe63339bbef049c6b8c48bf8eec71829dac57b43a04b93ae84e29efa6f3 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/IndexedDigest.java
+56b2c1a1d862402f8e36ec9f46658b6c6ab960841b45554ff7fc80cf98ff8877 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/JdkDigest.java
+d55e72f4a50bbd61265f062fbdc78f81e3df95e885d97b7a330767d0ac118282 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/NodeEntry.java
+2390c4e1582e3851206822734508bd9e1e09ac214a60ad5df4e58fbd20d62862 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/PK.java
+048b42bc48fefbe1f5078bd07c79c1dfd0681dbc9f28faca079044c5a0164446 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ParallelSigning.java
+bb22f53b9a859f8aaeb6218ce552b9808c75f0b3dc669cfb96b1d7f4eb2a15ef consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG.java
+ae0dbd055801e5ee56c5a239dd4ef053865429f7cf986fc5bb133f132d75e5a5 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_FORS.java
+b0052acfe28315a1ef049a67827152d78c35003188a40df279894a6310bec576 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_XMSS.java
+56f23200ef1ec3db3f2403df56400cf97520dad2766f5ade3808cfbbba21ad3a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SK.java
+20d6a4c38a925d98d89d3ce200eb726b53b9490251c799ff0ca7882b1715aad7 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngine.java
+14cd5293a9305f408eb4997bb2feaffeae4e3fbb150319c7cff622d35eec2cc8 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngineProvider.java
+a8be3a968c50693252e82b705180988b50b62a360045c2463aa7bc9f1585dd13 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyGenerationParameters.java
+d2a9bf3fedeb52712d73e1ca18536a9ea2e94f935b525b1cf31fdff09fe810d7 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyPairGenerator.java
+93bf21d8c5a68d29197ac8b884c49fe9180e1cd72929405c50b24530c87dbc0d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyParameters.java
+dc052f82c66f852bf327115070f11bf1d7aea0acb89b52b2e4682e6f1e039a75 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAParameters.java
+fa85313f90f2e92b338d9c3f6df4cb57a7583fe8c82417f19928a133932dfd18 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPrivateKeyParameters.java
+743d78ba0f9f553b02da222cf025882e1774694b5ab72a8e2b1581ddf6035eae consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPublicKeyParameters.java
+6f084a5acf5e89834218a6fae120c4542a86250e263d2fa52c6f04e5183fdd7f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.java
+2cd85f1517fe9770558ffef6251600bce76609a96beb21f0a8f120412b2a935f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java
6ace00e18914a1558563e689b7427654b5e6226a31447bef1d0416d993f78840 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java
ed1cc6f617dc5a2f7801b6e8f6f039a035a2ffc9542b116262dac39bb70563dd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryV3Tool.java
cf0dcc008ce2d040590329088abdcbfc0829bf529445422deee8022d4856e08a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelayTest.java
f5d6d26b9cd3babad8f7d16b9595497908b1f854d166837fbecd485d721ae856 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereKeysFromV3Test.java
1d7e3a30d3f4653b778a1bcbb81203ed8855d904017fa438a06e26baad8958c3 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereLocalKeysV3Test.java
27e5782c1742a361db7a22269ac44c1af574e25b07a87c76c10792bc9741b65c consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java
+ce25387272397b5aa0d53039b4cd119782081da36a282e40c23c95b7397ad62f consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTagsTest.java
15d2d2cfc627406ae6183353a81703fd40da871b216ff8e9b1c4d15af2437805 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqIdentityTest.java
acd5c1e7f49dbbf444c19c91386bbd03b9298d952d2d13721793670103e71ee9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D177NeutralNamesTest.java
c574ec22e642ac464b1728d27e4bb26202448a45c044f08935cc0a0efa0297d9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FalconAttachIntervalTest.java
e96c6e79287a17eafef8f4ff5c32e539e489115b02f7afaceb8cdeff73562d69 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
+0e89bd1444821a4b59289c45782b7c70b7621101059ea4cb8aa89bc3280b8e76 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java
2920f18dae0eb7ccead43d80202580b116e28c9fae5bb75d54917ddb99b9c2e9 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
@@ -101,7 +105,7 @@ f260d284f936307ac4c31142cbea0359ded1197880b8a960ac27dd7beb5c3b53 consensus/comm
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
05423fc69ce0e4e94cda0d56a181a996320bd2166af3415f2d88102cf3b0ae21 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryV3Test.java
-763dcb09c30c2031ac19bb62ec7a8a16a2f8042fd0e3d04840867b08df5b1ba3 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java
+d7d675be2a5ee598ce74f58156383589a05b8e977c2cc790671e3a39c7728309 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java
e73a075b2acfce7a6d2de63130ed55a3d8f422bdc72c51325a010852064d406d 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
@@ -112,11 +116,12 @@ e355b147fdda48c14cb6dc09b1bc1e7a7ccae3f9b709af8f8483f896086dbe4d consensus/comm
d48d62c6ea0784864a5987a804a28855265b2180c409ba3c5b4434d21b805e1d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java
184e3cc6f2f9beb1bd972b077ab2ced456264a55296f810ba5ed2dc976195392 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SlhDsaCrossVectorTest.java
ae37d3525697bf84defd796bb8813193c78f81e7fa24a72f8f048285d52399e2 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SlhDsaFastEngineTest.java
+d0ba00d95427ae4fb1c0a1eeb37bcd039fbfa5412957dc4b2da0725c0dea5028 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SlhDsaParallelSigningTest.java
bcdd5c3c267f38eee9700bc6ba07d41b487c7889224820868e1839044108451f consensus/common/src/test/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTallyIdentityTest.java
6b6ecfcfb9b25bb009cb5d4df74648c729abf62626e5034af126771825b7418e consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Commit.java
-597d96d64c9394c79d5084a1ab5df884b2266efe315c0f2d563bfc17b7421480 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java
+a1405402e891189909a436abd9f83c863349fdcf01d3011542d805e2bb3911ed consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java
35d38accf0d22b6fdadeab4f2bce0cdac708f8115591594b1bb1fa385b227c5c consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java
-d365bad33f3aee4597c630071ca1f727ddd8bb6ab6515fccbce0a07842037adf consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java
+536375fa2d9f27107d9907f34391413e052f2a8cd89611033fefe29b9edc7891 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java
7914cc3f4a8a85dc7e2e8dad1ea9add86c4df00b04c2f7d822d4fed60a3aadcb consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayload.java
8f8b041e72cfaee43d2f4e61a790e603ed35b329cf431de68fc6907a18c3b824 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java
e19b12e6db33cfb9bb1c2f22e321815130dfbcc5730bb505320f08a45a371093 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java
@@ -166,7 +171,7 @@ dc9f9e862a11f0135d26976176a1d1adac3e84fd5f1e06572d8727262cb3374f consensus/qbft
83dd971f66ed63103d09db5283240556bf1db1e1925c2a07808c7f59d47433ad consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangeJustificationPqTest.java
8f6abb4297df964e934cfeabe51f365478e917b73cd6909f621c9e201e280aa5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidatorPqWiringTest.java
24ee993a415eea57c80fe2c9b839ce20140bfcc580e269e7b1182f71e960c1f0 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java
-65ba290ae597b2217b86f2dd4b9db5faacc6e60ef7e36343a9d724c40b66dd73 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java
+ebf6da56130720ea95c64505959d64bfba46540ebf2139c3f71a7051d88837a1 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java
c870d39a19174328bc394658f4a365a61bd57fe7b0e28e6c5e4aaeb28bb509e5 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
@@ -176,7 +181,7 @@ ab0fcd8722dcb76560f0ef8fda8af9c2b8b6ec9b468326aaf616c853526f7f81 consensus/qbft
0f348ac4fa519f27f52f631dd290eab6e84980739a09ec767c8ca69e4a4f8f64 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
a76a87987050ea6765e2a1f43d8f828ca8aec218c951e395e7acb62465e8b2fd consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRule.java
2add2a7733668e11b50ddd64b05bb37db26a18284a636bbe720c2cb1f6aa28ed consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRule.java
-47dd0c84b2f1dba800ec843a0c06f85db86825e06493df13ffca3d5b5a9219dc consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java
+e0f8afc69fef826c5ee8a95745a967b1049ce10b9d591696a4fc095fb28062cb consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java
8ac99e94c89f61f0281199cf369e282fc4f9fb1a3414a1650766e1d7548e76f5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java
5dcf5c22389c063e12363b83366b39bd3b2e79f36c68e777fb3ef11080f53ea9 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/AereIdentityHeaderTest.java
52b56f157500ae3527b7e55c51786915bb5d6980065668f1e7b8e297bf7125ef consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftAnchorRuleWiringTest.java
@@ -192,7 +197,7 @@ a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft
9c4e6a56ecffa70ebb03f769eda8c161d70eed47935488b88cb30c1bafa15a73 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRuleTest.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
-2db85c0c1b080123b252b6fb8896266d9d66753cf352826d3ed72c0790973abb consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java
+8d0113eec652dc5e08600fb7fb07de3e08524a500ea6bdcf3fd0f46f3f1b38e6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java
7ea99f24bc1c9131e8ef08f7a2c6f2a08af71ebee011a4cb88a367bd446b6c27 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java
5bf41ab0be06328fa1e18bf9c98bfd89b7b16384fe185d96b3385595dabe61b3 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetCode.java
4c96b06073ff3830f4e0292fc732ec3fb9e0ed50ef741dceedf755b50f18d5ae ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetStorageAt.java
@@ -201,3 +206,4 @@ a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft
3ee087fc34b455e113ec9d2c53b5dbdb3574429fcf166f4c968a80120f70f0eb ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AereStateUnavailableIsAnErrorTest.java
6e99e9f399558abbd1d4fdb4b176ba413bf31cabb3c8aba1701c17ce5828ce8a ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/AereHeadStateRetryTest.java
3b690b72e80a0f9eaf868a3fa3f99ace42dad74dc30b2c98b9f3ca5a023917c0 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadHeadersStep.java
+7f611966d74f52ef2f6d55bb5882ffb72b10abf1985df2b01f03aad4a6e2baf1 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/PropagatedBlockAdoptionGate.java
diff --git a/anchor/app/src/main/java/org/hyperledger/besu/controller/PqTipAdoptionGate.java b/anchor/app/src/main/java/org/hyperledger/besu/controller/PqTipAdoptionGate.java
new file mode 100644
index 0000000..19f3692
--- /dev/null
+++ b/anchor/app/src/main/java/org/hyperledger/besu/controller/PqTipAdoptionGate.java
@@ -0,0 +1,187 @@
+/*
+ * 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.controller;
+
+import org.hyperledger.besu.consensus.common.bft.AerePqHeaderSeal;
+import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
+import org.hyperledger.besu.consensus.common.bft.BftHelpers;
+import org.hyperledger.besu.consensus.common.bft.FalconSeal;
+import org.hyperledger.besu.consensus.common.bft.PqAnchor;
+import org.hyperledger.besu.consensus.common.bft.PqSealCache;
+import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
+import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+import org.hyperledger.besu.ethereum.eth.sync.PropagatedBlockAdoptionGate;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.apache.tuweni.bytes.Bytes32;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AERE D-412 (2026-09-26), Rule C, the consensus-aware implementation of {@link
+ * PropagatedBlockAdoptionGate.Gate}.
+ *
+ *
A validator does not ADOPT, as the new canonical head, a block that arrived by propagation at the
+ * tip of its own consensus height unless it holds a post-quantum COMMIT quorum for that exact block
+ * hash - the Falcon-512 commit seals it heard on AIP-21 Commit messages, re-verified now against the
+ * anchored registry. From H_hdr the header carries only the proposer's single seal, so a valid header no
+ * longer proves quorum agreement; that agreement lives in the seal cache, and a single-key forgery has
+ * none. The block stays VALID (this is fork choice, not validation), so:
+ *
+ *
+ *
Below H_hdr the gate is inert (the header's ECDSA committed-seals rule already proves a quorum).
+ *
A reader (not in the validator set) is not gated: it hears no commit seals, so it could never
+ * clear the gate and would stall; its canonicity is Rule B, out of this milestone's scope.
+ *
The sync downloader is a different code path and does not consult this gate, so a validator that
+ * fell behind catches up freely and only rejoins the gated tip once it hears commits again.
+ *
+ *
+ *
The quorum proof is cryptographic: each cached seal is re-verified with Falcon over {@link
+ * PqAnchor#commitMessage} (the exact message a commit seal signs from the anchor form, which holds at
+ * every height at or above H_hdr), and only distinct verified signer indexes are counted, so junk in the
+ * cache cannot forge the proof. To clear the gate an adversary would need the commit quorum's distinct
+ * Falcon signatures over the block's commit message - which is exactly the agreement D-412 shows a single
+ * key cannot produce.
+ */
+public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PqTipAdoptionGate.class);
+
+ /** Verifies one Falcon seal over the commit message of a block; injectable for tests. */
+ @FunctionalInterface
+ public interface SealVerifier {
+ /**
+ * Verify.
+ *
+ * @param blockNumber the height
+ * @param validatorIndex the signer index in the anchored registry
+ * @param message the commit message that was signed
+ * @param signature the Falcon signature
+ * @return true iff it verifies against the key bound at that height
+ */
+ boolean verify(long blockNumber, int validatorIndex, Bytes message, Bytes signature);
+ }
+
+ private final ValidatorProvider validatorProvider;
+ private final Address localEcdsaAddress;
+ private final long chainId;
+ private final PqSealCache cache;
+ private final SealVerifier verifier;
+
+ /**
+ * Production wiring.
+ *
+ * @param validatorProvider the validator provider (the set at a height; ids from H_id, addresses below)
+ * @param localEcdsaAddress this node's ECDSA address (from its node key)
+ * @param chainId the chain id every commit message carries
+ */
+ public PqTipAdoptionGate(
+ final ValidatorProvider validatorProvider, final Address localEcdsaAddress, final long chainId) {
+ this(
+ validatorProvider,
+ localEcdsaAddress,
+ chainId,
+ PqSealCache.instance(),
+ (n, i, m, s) -> PqSignerRegistry.falconSealSupport().verifyAtHistoric(n, i, m, s));
+ }
+
+ /**
+ * Full wiring, for tests.
+ *
+ * @param validatorProvider the validator provider
+ * @param localEcdsaAddress this node's ECDSA address
+ * @param chainId the chain id
+ * @param cache the seal cache to read
+ * @param verifier the seal verifier
+ */
+ PqTipAdoptionGate(
+ final ValidatorProvider validatorProvider,
+ final Address localEcdsaAddress,
+ final long chainId,
+ final PqSealCache cache,
+ final SealVerifier verifier) {
+ this.validatorProvider = validatorProvider;
+ this.localEcdsaAddress = localEcdsaAddress;
+ this.chainId = chainId;
+ this.cache = cache;
+ this.verifier = verifier;
+ }
+
+ @Override
+ public boolean mayAdopt(final BlockHeader header, final BlockHeader parent) {
+ final long h = header.getNumber();
+ // Below H_hdr the header still carries a quorum of ECDSA committed seals, whose upstream rule
+ // already refuses a single-key block; nothing to add here.
+ if (!AerePqHeaderSeal.active(h)) {
+ return true;
+ }
+ final Collection
validators = validatorProvider.getValidatorsForBlock(parent);
+ if (validators == null || validators.isEmpty()) {
+ return true;
+ }
+ // Only a validator hears commit seals; a reader would never clear the gate, so it is not gated
+ // (its canonicity is Rule B). isLocal matches this node as an ECDSA address OR its v3 id, so it is
+ // correct below H_id (addresses) and from stage 5 (ids), where the set carries ids.
+ boolean localIsValidator = false;
+ for (final Address v : validators) {
+ if (AerePqIdentity.isLocal(v, localEcdsaAddress)) {
+ localIsValidator = true;
+ break;
+ }
+ }
+ if (!localIsValidator) {
+ return true;
+ }
+ final int quorum = BftHelpers.calculateRequiredValidatorQuorum(validators.size());
+ if (quorum <= 0) {
+ return true;
+ }
+ final Bytes32 message = PqAnchor.commitMessage(chainId, h, header.getHash().getBytes());
+ final List heard = cache.sealsFor(h, header.getHash());
+ final Set verified = new HashSet<>();
+ for (final FalconSeal seal : heard) {
+ if (seal == null || seal.getValidatorIndex() < 0 || seal.getSignature() == null) {
+ continue;
+ }
+ try {
+ if (verifier.verify(h, seal.getValidatorIndex(), message, seal.getSignature())) {
+ verified.add(seal.getValidatorIndex());
+ }
+ } catch (final RuntimeException e) {
+ // a seal this node cannot judge is not counted; never a pass
+ LOG.debug("AERE D-412: seal of index {} for block {} could not be verified ({})", seal.getValidatorIndex(), h, e.toString());
+ }
+ }
+ final boolean ok = verified.size() >= quorum;
+ if (!ok) {
+ LOG.info(
+ "AERE D-412: propagated block {} at tip height {} holds {} verified post-quantum commit "
+ + "seal(s), quorum is {}; NOT adopting it via propagation - this node's own consensus "
+ + "decides this height.",
+ header.getHash(),
+ h,
+ verified.size(),
+ quorum);
+ }
+ return ok;
+ }
+}
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 d9ef6bb..d7c826c 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
@@ -31,6 +31,7 @@ import org.hyperledger.besu.consensus.common.BftValidatorOverrides;
import org.hyperledger.besu.consensus.common.EpochManager;
import org.hyperledger.besu.consensus.common.ForksSchedule;
import org.hyperledger.besu.consensus.common.bft.AerePqFinality;
+import org.hyperledger.besu.consensus.common.bft.AerePqFinalityTags;
import org.hyperledger.besu.consensus.common.bft.AerePqHeaderSeal;
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
import org.hyperledger.besu.consensus.common.bft.BftBlockInterface;
@@ -113,6 +114,7 @@ import org.hyperledger.besu.ethereum.eth.EthProtocol;
import org.hyperledger.besu.ethereum.eth.SnapProtocol;
import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManager;
import org.hyperledger.besu.ethereum.eth.manager.snap.SnapProtocolManager;
+import org.hyperledger.besu.ethereum.eth.sync.PropagatedBlockAdoptionGate;
import org.hyperledger.besu.ethereum.eth.sync.state.SyncState;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
@@ -608,6 +610,44 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder {
protocolContext.getConsensusContext(BftContext.class).getValidatorProvider();
}
+ // AERE D-412 (2026-09-26), Rule C: arm the propagation adoption gate. From AIP-22 stage 5 a valid
+ // header carries only the proposer's single Falcon seal, so it no longer proves a quorum agreed the
+ // block; a single key could forge one and, until this gate, honest validators adopted it via NewBlock
+ // and built on top (measured on rep28098). The gate lets this node refuse to ADOPT such a propagated
+ // block at the tip of its own consensus height unless it holds a post-quantum commit quorum for it
+ // (heard on AIP-21 Commit seals). Block VALIDITY is unchanged (sync still imports it); only
+ // ADOPTION-at-the-tip is gated, only for a validator, only from H_hdr. Inert here on any chain whose
+ // H_hdr is unset, which is chain 2800 today. Placed after the validator set is resolved (localAddress
+ // and validatorProvider both exist) and before the network and the QBFT machine start.
+ if (aereAnchorConfig.everActive()) {
+ PropagatedBlockAdoptionGate.install(
+ new PqTipAdoptionGate(validatorProvider, localAddress, aereAnchorConfig.chainId()));
+ LOG.info(
+ "AERE D-412: propagation adoption gate armed; a propagated block at the tip is adopted only "
+ + "with a post-quantum commit quorum from H_hdr={} (unset means never on this node).",
+ AerePqHeaderSeal.headerBlock());
+ }
+
+ // AERE D-412 (2026-09-26), Rule B1: post-quantum FINALITY TAGS (see AerePqFinalityTags). finalized/safe = the parent of the
+ // last canonical anchor that carries a certificate (v1 Falcon list OR v2 scheme-tagged certificate); monotonic; never moved
+ // back by a reorg (AERE-PQC-FIN-01). No validity, fork-choice or import is touched. Proven live on rep28098 2026-09-26.
+ if (aereAnchorConfig.everActive()) {
+ final QbftExtraDataCodec codecForTags = qbftExtraDataCodec;
+ AerePqFinalityTags.install(
+ aereAnchorConfig,
+ blockchain,
+ h -> {
+ try {
+ return AerePqFinalityTags.carriesCertificate(codecForTags.decodeRaw(h.getExtraData()));
+ } catch (final RuntimeException e) {
+ return false; // an undecodable header carries nothing this rule can rely on
+ }
+ });
+ LOG.info(
+ "AERE D-412 B1: finalized/safe tags follow the last certified anchor's parent (interval {} at the head).",
+ aereAnchorConfig.intervalAt(blockchain.getChainHeadBlockNumber()));
+ }
+
// AERE GARDA-PRAG (2026-08-02): refuse to start on a seal threshold that STOPS the chain.
//
// Measured before this line was written: nothing anywhere refused a threshold value. Searched
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTags.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTags.java
new file mode 100644
index 0000000..e5e11c9
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTags.java
@@ -0,0 +1,202 @@
+/*
+ * 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 org.hyperledger.besu.datatypes.Hash;
+import org.hyperledger.besu.ethereum.chain.BlockAddedEvent;
+import org.hyperledger.besu.ethereum.chain.MutableBlockchain;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+// in this Besu base (26.4.0-dev, d2032017b) the event type enum lives in the plugin API, not nested in BlockAddedEvent
+// (measured 2026-09-26: "cannot find symbol EventType in class BlockAddedEvent" on the first harness run)
+import org.hyperledger.besu.plugin.data.AddedBlockContext.EventType;
+
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Predicate;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AERE D-412 Rule B1 (2026-09-26): the post-quantum FINALITY TAGS.
+ *
+ *
What this closes, read in the code and measured on the chain. From AIP-22 stage 5 a valid
+ * between-anchor header carries only the proposer's Falcon-512 seal, so a single validator key can
+ * forge a header a reader imports as valid and, if that reader is syncing or eclipsed, adopts as its
+ * canonical tip (D-412, corrected severity). Such a forged tail is bounded: at the next anchor height
+ * {@code PqAnchorSealsRule} (R2, FULL validation, which {@code PqAnchorSyncModeGuard} makes the only
+ * sync mode) requires K distinct valid Falcon-512 seals over the parent's commit message, which one key
+ * cannot produce, so the tail dies within one anchor interval. What remains is that the reader's
+ * {@code latest} can transiently name a forged block, and a consumer who asks for {@code finalized} or
+ * {@code safe} - the tags bridges, exchanges and rollups use - received, on every node of chain 2800 and
+ * 28001 measured 2026-09-26, the error {@code Unknown block}: QBFT never sets them.
+ *
+ *
The rule. {@code finalized} and {@code safe} name the last block COVERED BY A VALID ANCHOR
+ * CERTIFICATE: when a canonical head at an anchor height carries a certificate, the anchor's PARENT
+ * becomes finalized (the certificate's K seals sign the parent's commit message, AIP-15, and R1/R2 have
+ * already refused the header if that certificate does not bind or does not verify). Between anchors
+ * the tags do not move. A forged between-anchor block can therefore never be reported final: it is
+ * never an anchor's certified parent. This is exactly the finality AIP-22 section 1 defines ("the
+ * finality of the block is established only by the covering anchor"), exposed on the API the world
+ * already reads.
+ *
+ *
What it deliberately does NOT do. It does not touch fork-choice, block validity or import:
+ * nothing here can stop the chain or refuse a block, and a node that lags still catches up exactly as
+ * before. It only writes the two tags, monotonically: a later event can only move them forward. If a
+ * canonical reorg ever drops below the finalized block (which would mean K validators signed two
+ * conflicting anchors, outside this rule's adversary), it is logged as {@code AERE-PQC-FIN-01} at
+ * ERROR and the tags are left where they were, never moved back in silence.
+ *
+ *
At startup the tags are initialised by scanning back from the head to the most recent
+ * anchor height that carries a certificate (at most two intervals), so a freshly started node answers
+ * {@code finalized} before it imports its next anchor.
+ *
+ *
Inert on a chain where the anchor was never active (the node never installs it); inert below
+ * the activation height (no anchor height applies there). Stated, not measured here: the behaviour on
+ * the live fleet, which is the rehearsal's job.
+ */
+public final class AerePqFinalityTags {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AerePqFinalityTags.class);
+
+ /** Logged when a canonical reorg would move finality backwards; the tags are not moved. */
+ public static final String CODE_REORG_BELOW_FINALIZED = "AERE-PQC-FIN-01";
+
+ private final PqAnchorConfig config;
+ private final MutableBlockchain blockchain;
+ private final Predicate carriesCertificate;
+ private final AtomicLong finalizedNumber = new AtomicLong(-1L);
+
+ AerePqFinalityTags(
+ final PqAnchorConfig config,
+ final MutableBlockchain blockchain,
+ final Predicate carriesCertificate) {
+ this.config = config;
+ this.blockchain = blockchain;
+ this.carriesCertificate = carriesCertificate;
+ }
+
+ /**
+ * Install the tags on a blockchain: initialise from the current head, then follow every block added.
+ *
+ * @param config the anchor configuration (activation height, interval schedule)
+ * @param blockchain the chain whose finalized/safe tags are written
+ * @param carriesCertificate whether a header carries a (validated) anchor certificate
+ * @return the installed instance, for inspection
+ */
+ public static AerePqFinalityTags install(
+ final PqAnchorConfig config,
+ final MutableBlockchain blockchain,
+ final Predicate carriesCertificate) {
+ final AerePqFinalityTags tags = new AerePqFinalityTags(config, blockchain, carriesCertificate);
+ tags.initialiseFromHead();
+ blockchain.observeBlockAdded(tags::onBlockAdded);
+ return tags;
+ }
+
+ /** The height currently reported as finalized, or -1 when none has been set by this rule. */
+ public long finalizedNumber() {
+ return finalizedNumber.get();
+ }
+
+ /**
+ * Whether a decoded header carries an anchor certificate. A header carries the v1 Falcon list OR
+ * the v2 scheme-tagged certificate, never both ({@link BftExtraData}), and from AIP-22 stage 5
+ * every anchor carries the v2 certificate while {@code getFalconSeals()} is empty. So a predicate
+ * that only looked at the v1 list ({@code getFalconSeals()}) never fired on a stage-5 chain -
+ * measured 2026-09-26 on the rep28098 rehearsal, where {@code finalized} stayed 0 while the chain
+ * ran past certified anchors 1920 and 1952. Both fields are checked here, in one place both the
+ * controller and the test read, so the two cannot drift.
+ *
+ * @param extraData the decoded BFT extra data of the header
+ * @return true when the header carries a (v1 or v2) anchor certificate
+ */
+ public static boolean carriesCertificate(final BftExtraData extraData) {
+ return !extraData.getFalconSeals().isEmpty() || !extraData.getHybridSeals().isEmpty();
+ }
+
+ /** Scan back from the head, at most two anchor intervals, to the last certified anchor. */
+ void initialiseFromHead() {
+ final BlockHeader head = blockchain.getChainHeadHeader();
+ if (head == null) {
+ return;
+ }
+ final long top = head.getNumber();
+ final long floor = Math.max(0L, top - 2L * Math.max(1, config.intervalAt(top)));
+ for (long h = top; h >= floor; h--) {
+ if (!config.anchorAppliesAt(h)) {
+ continue;
+ }
+ final Optional header = blockchain.getBlockHeader(h);
+ if (header.isPresent() && carriesCertificate.test(header.get())) {
+ set(header.get().getParentHash(), h - 1L, "startup scan");
+ return;
+ }
+ }
+ LOG.info(
+ "AERE PQ FINALITY (B1): no certified anchor between {} and the head {}; finalized/safe stay "
+ + "unset until the next anchor is imported.",
+ floor,
+ top);
+ }
+
+ /**
+ * Follow a block added to the chain.
+ *
+ * @param event the event
+ */
+ void onBlockAdded(final BlockAddedEvent event) {
+ if (event == null || !event.isNewCanonicalHead()) {
+ return;
+ }
+ final BlockHeader header = event.getHeader();
+ if (header == null) {
+ return;
+ }
+ if (event.getEventType() == EventType.CHAIN_REORG && finalizedNumber.get() >= 0) {
+ final Hash ancestorHash = event.getCommonAncestorHash();
+ final Optional ancestor =
+ ancestorHash == null ? Optional.empty() : blockchain.getBlockHeader(ancestorHash);
+ if (ancestor.isPresent() && ancestor.get().getNumber() < finalizedNumber.get()) {
+ LOG.error(
+ "{}: a canonical reorg to {} (common ancestor {}) went BELOW the finalized block {}. "
+ + "Finality tags are NOT moved back. Under the anchor rules this needs K validators to "
+ + "have certified two conflicting anchors; investigate before anything else.",
+ CODE_REORG_BELOW_FINALIZED,
+ header.getNumber(),
+ ancestor.get().getNumber(),
+ finalizedNumber.get());
+ }
+ }
+ final long h = header.getNumber();
+ if (config.anchorAppliesAt(h) && carriesCertificate.test(header)) {
+ set(header.getParentHash(), h - 1L, "anchor " + h);
+ }
+ }
+
+ private void set(final Hash finalHash, final long finalNumber, final String why) {
+ if (finalNumber <= finalizedNumber.get()) {
+ return; // monotonic: never backwards, never twice for the same height
+ }
+ blockchain.setFinalized(finalHash);
+ blockchain.setSafeBlock(finalHash);
+ finalizedNumber.set(finalNumber);
+ LOG.info(
+ "AERE PQ FINALITY (B1): finalized/safe = {} ({}), covered by the certificate of {}.",
+ finalNumber,
+ finalHash,
+ why);
+ }
+}
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 14071c5..3617715 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
@@ -168,10 +168,10 @@ public final class PqRegistryHashTool {
epoci.put(Long.parseLong(rot.substring(0, eq).trim()), Path.of(rot.substring(eq + 1).trim()));
}
- final List inaltimi = new ArrayList<>(epoci.keySet());
- inaltimi.sort(Long::compare);
- if (inaltimi.get(0) != h) {
- System.out.println("RED: the first epoch is at " + inaltimi.get(0) + ", not at the arming height " + h);
+ final List epochHeights = new ArrayList<>(epoci.keySet());
+ epochHeights.sort(Long::compare);
+ if (epochHeights.get(0) != h) {
+ System.out.println("RED: the first epoch is at " + epochHeights.get(0) + ", not at the arming height " + h);
return 1;
}
@@ -186,8 +186,8 @@ public final class PqRegistryHashTool {
json.append(" \"pqRegistryHash\": [\n");
final StringBuilder history = new StringBuilder();
final StringBuilder harta = new StringBuilder();
- for (int i = 0; i < inaltimi.size(); i++) {
- final long b = inaltimi.get(i);
+ for (int i = 0; i < epochHeights.size(); i++) {
+ final long b = epochHeights.get(i);
final Path p = epoci.get(b);
final PqRegistryHash.Registry reg = PqRegistryHash.loadAuto(p);
// AERE D-C (2026-08-06). THIS IS THE DANGEROUS VERB: what is printed here gets pasted into
@@ -222,7 +222,7 @@ public final class PqRegistryHashTool {
return 1;
}
json.append(" {\"block\": ").append(b).append(", \"hash\": \"0x").append(hash).append("\"}");
- if (i < inaltimi.size() - 1) {
+ if (i < epochHeights.size() - 1) {
json.append(',');
}
json.append('\n');
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
index 7610cf8..ca87df8 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
@@ -2,10 +2,12 @@
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
- * The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
- * intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
- * cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
- * FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
+ * Changes of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI intrinsics)
+ * instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature cost seconds on
+ * the QBFT thread at every anchor parent; and (2026-09-26) signing first tries ParallelSigning, which
+ * spreads the message-independent work over the cores and gives the same bytes (HT gains a constructor
+ * without the unused top-layer public key). Algorithm, key formats and signature bytes are FIPS 205 as
+ * before; SlhDsaFastEngineTest and SlhDsaParallelSigningTest pin that against the original.
* DO NOT EDIT BY HAND: regenerate with the script above.
*/
package org.hyperledger.besu.consensus.common.bft.slhdsa;
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
index d89d83a..803fca9 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
@@ -2,10 +2,12 @@
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
- * The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
- * intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
- * cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
- * FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
+ * Changes of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI intrinsics)
+ * instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature cost seconds on
+ * the QBFT thread at every anchor parent; and (2026-09-26) signing first tries ParallelSigning, which
+ * spreads the message-independent work over the cores and gives the same bytes (HT gains a constructor
+ * without the unused top-layer public key). Algorithm, key formats and signature bytes are FIPS 205 as
+ * before; SlhDsaFastEngineTest and SlhDsaParallelSigningTest pin that against the original.
* DO NOT EDIT BY HAND: regenerate with the script above.
*/
package org.hyperledger.besu.consensus.common.bft.slhdsa;
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
index b7d96c1..88ff081 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
@@ -2,10 +2,12 @@
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
- * The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
- * intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
- * cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
- * FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
+ * Changes of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI intrinsics)
+ * instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature cost seconds on
+ * the QBFT thread at every anchor parent; and (2026-09-26) signing first tries ParallelSigning, which
+ * spreads the message-independent work over the cores and gives the same bytes (HT gains a constructor
+ * without the unused top-layer public key). Algorithm, key formats and signature bytes are FIPS 205 as
+ * before; SlhDsaFastEngineTest and SlhDsaParallelSigningTest pin that against the original.
* DO NOT EDIT BY HAND: regenerate with the script above.
*/
package org.hyperledger.besu.consensus.common.bft.slhdsa;
@@ -24,6 +26,18 @@ class HT
final byte[] htPubKey;
+ // AERE (D-337, 2026-09-26): the same object without htPubKey. The constructor below computes the
+ // top-layer public key (a full treehash of 2^H' WOTS+ leaves) on every signature, and signing never
+ // reads it. Used by ParallelSigning; key generation keeps the constructor below.
+ HT(SLHDSAEngine engine, byte[] skSeed, byte[] pkSeed, boolean withoutPublicKey)
+ {
+ this.skSeed = skSeed;
+ this.pkSeed = pkSeed;
+ this.engine = engine;
+ this.wots = new WotsPlus(engine);
+ this.htPubKey = null;
+ }
+
public HT(SLHDSAEngine engine, byte[] skSeed, byte[] pkSeed)
{
this.skSeed = skSeed;
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/IndexedDigest.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/IndexedDigest.java
index 8ff076d..49bf358 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/IndexedDigest.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/IndexedDigest.java
@@ -2,10 +2,12 @@
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
- * The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
- * intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
- * cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
- * FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
+ * Changes of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI intrinsics)
+ * instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature cost seconds on
+ * the QBFT thread at every anchor parent; and (2026-09-26) signing first tries ParallelSigning, which
+ * spreads the message-independent work over the cores and gives the same bytes (HT gains a constructor
+ * without the unused top-layer public key). Algorithm, key formats and signature bytes are FIPS 205 as
+ * before; SlhDsaFastEngineTest and SlhDsaParallelSigningTest pin that against the original.
* DO NOT EDIT BY HAND: regenerate with the script above.
*/
package org.hyperledger.besu.consensus.common.bft.slhdsa;
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/JdkDigest.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/JdkDigest.java
index c7a8e86..7edcab8 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/JdkDigest.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/JdkDigest.java
@@ -2,10 +2,12 @@
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
- * The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
- * intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
- * cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
- * FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
+ * Changes of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI intrinsics)
+ * instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature cost seconds on
+ * the QBFT thread at every anchor parent; and (2026-09-26) signing first tries ParallelSigning, which
+ * spreads the message-independent work over the cores and gives the same bytes (HT gains a constructor
+ * without the unused top-layer public key). Algorithm, key formats and signature bytes are FIPS 205 as
+ * before; SlhDsaFastEngineTest and SlhDsaParallelSigningTest pin that against the original.
* DO NOT EDIT BY HAND: regenerate with the script above.
*/
package org.hyperledger.besu.consensus.common.bft.slhdsa;
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/NodeEntry.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/NodeEntry.java
index 4cd30cf..adaa9ab 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/NodeEntry.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/NodeEntry.java
@@ -2,10 +2,12 @@
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
- * The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
- * intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
- * cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
- * FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
+ * Changes of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI intrinsics)
+ * instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature cost seconds on
+ * the QBFT thread at every anchor parent; and (2026-09-26) signing first tries ParallelSigning, which
+ * spreads the message-independent work over the cores and gives the same bytes (HT gains a constructor
+ * without the unused top-layer public key). Algorithm, key formats and signature bytes are FIPS 205 as
+ * before; SlhDsaFastEngineTest and SlhDsaParallelSigningTest pin that against the original.
* DO NOT EDIT BY HAND: regenerate with the script above.
*/
package org.hyperledger.besu.consensus.common.bft.slhdsa;
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/PK.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/PK.java
index 4d0ef5f..8e4b32e 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/PK.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/PK.java
@@ -2,10 +2,12 @@
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
- * The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
- * intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
- * cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
- * FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
+ * Changes of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI intrinsics)
+ * instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature cost seconds on
+ * the QBFT thread at every anchor parent; and (2026-09-26) signing first tries ParallelSigning, which
+ * spreads the message-independent work over the cores and gives the same bytes (HT gains a constructor
+ * without the unused top-layer public key). Algorithm, key formats and signature bytes are FIPS 205 as
+ * before; SlhDsaFastEngineTest and SlhDsaParallelSigningTest pin that against the original.
* DO NOT EDIT BY HAND: regenerate with the script above.
*/
package org.hyperledger.besu.consensus.common.bft.slhdsa;
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ParallelSigning.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ParallelSigning.java
new file mode 100644
index 0000000..2a1505d
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ParallelSigning.java
@@ -0,0 +1,257 @@
+/*
+ * 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
+ *
+ * Written by AERE Network (not derived from Bouncy Castle); emitted into this package by
+ * consensus-pqc/slhdsa-fork/genereaza-motorul.py from ParallelSigning.java.sablon. DO NOT EDIT BY HAND.
+ */
+package org.hyperledger.besu.consensus.common.bft.slhdsa;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.bouncycastle.util.Arrays;
+
+/**
+ * SLH-DSA signing with the message-independent work spread over the cores; the SAME bytes as the
+ * sequential signer, which remains the fallback.
+ *
+ *
D-337, measured 2026-09-26 on chain 2800: the parent of every anchor took 1.69 s against 0.60 s
+ * for an ordinary block (import times on two validators, fifteen anchors each), because each
+ * validator signs its Commit with SLH-DSA-SHA2-128s there, on the QBFT thread, on one core. One
+ * signature on a validator's CPU (EPYC-Rome, one core): 0.47 s warm, 0.88-1.02 s cold, and a
+ * validator signs once every ~77 s. Almost all of it is message-independent: the authentication
+ * path of each of the D hypertree layers (2^H' - 1 WOTS+ leaves each) and the k FORS trees. Only
+ * the WOTS+ signature of each layer depends on the root of the layer below, and that chain is cheap.
+ * So the auth paths and FORS run as separate tasks on a small daemon pool, each with its own engine
+ * (the engines hold digest state and are not thread-safe), and the chain is then walked in order.
+ *
+ *
Also dropped: the HT constructor used by the sequential signer computes the top layer's public
+ * key (a full 2^H'-leaf treehash) on every signature, and signing never reads it.
+ *
+ *
Taken only with more than one core and unless {@code -Daere.pq.slhdsa.parallel=false}; any
+ * failure returns null and the caller runs the original Bouncy Castle code unchanged.
+ */
+@SuppressWarnings("all")
+final class ParallelSigning {
+
+ static final String SWITCH_PROPERTY = "aere.pq.slhdsa.parallel";
+
+ private static final int THREADS =
+ Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors()));
+
+ // counts tasks executed on the pool; the proof reads it to show the parallel path was the one taken
+ private static final AtomicInteger TASKS_RUN = new AtomicInteger();
+
+ private static volatile ExecutorService pool;
+
+ private ParallelSigning() {}
+
+ static boolean enabled() {
+ return THREADS > 1
+ && !"false".equalsIgnoreCase(System.getProperty(SWITCH_PROPERTY, "true").trim());
+ }
+
+ static int tasksRun() {
+ return TASKS_RUN.get();
+ }
+
+ static int tasksPerSignature(final SLHDSAParameters params) {
+ return 1 + 2 * params.getEngine().D;
+ }
+
+ private static ExecutorService pool() {
+ ExecutorService p = pool;
+ if (p == null) {
+ synchronized (ParallelSigning.class) {
+ p = pool;
+ if (p == null) {
+ final AtomicInteger n = new AtomicInteger();
+ p =
+ Executors.newFixedThreadPool(
+ THREADS,
+ r -> {
+ final Thread t = new Thread(r, "aere-slhdsa-sign-" + n.incrementAndGet());
+ t.setDaemon(true);
+ return t;
+ });
+ pool = p;
+ }
+ }
+ }
+ return p;
+ }
+
+ /** The signature, or null when this path is not taken; the caller then runs the original code. */
+ static byte[] trySign(
+ final SLHDSAPrivateKeyParameters privKey,
+ final byte[] msgPrefix,
+ final byte[] msg,
+ final byte[] optRand) {
+ if (!enabled()) {
+ return null;
+ }
+ try {
+ return sign(privKey, msgPrefix, msg, optRand);
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return null;
+ } catch (final ExecutionException | RuntimeException e) {
+ return null;
+ }
+ }
+
+ static byte[] sign(
+ final SLHDSAPrivateKeyParameters privKey,
+ final byte[] msgPrefix,
+ final byte[] msg,
+ final byte[] optRand)
+ throws InterruptedException, ExecutionException {
+ final SLHDSAParameters params = privKey.getParameters();
+ final byte[] skSeed = privKey.sk.seed;
+ final byte[] pkSeed = privKey.pk.seed;
+ final SLHDSAEngine engine = params.getEngine();
+ engine.init(pkSeed);
+
+ // the randomiser and the message digest, exactly as SLHDSASigner.internalGenerateSignature
+ final byte[] r = engine.PRF_msg(privKey.sk.prf, optRand, msgPrefix, msg);
+ final IndexedDigest idxDigest = engine.H_msg(r, pkSeed, privKey.pk.root, msgPrefix, msg);
+ final byte[] mHash = idxDigest.digest;
+ final long idxTree = idxDigest.idx_tree;
+ final int idxLeaf = idxDigest.idx_leaf;
+ final int d = engine.D;
+ final int hPrime = engine.H_PRIME;
+
+ // the (tree, leaf) each layer signs with: the walk HT.sign does, layer 0 first
+ final long[] tree = new long[d];
+ final int[] leaf = new int[d];
+ long t = idxTree;
+ int l = idxLeaf;
+ for (int j = 0; j < d; j++) {
+ if (j > 0) {
+ l = (int) (t & ((1 << hPrime) - 1));
+ t >>>= hPrime;
+ }
+ tree[j] = t;
+ leaf[j] = l;
+ }
+
+ final List> tasks = new ArrayList>();
+ tasks.add(forsTask(params, skSeed, pkSeed, mHash, idxTree, idxLeaf));
+ final byte[][][] auth = new byte[d][hPrime][];
+ for (int j = 0; j < d; j++) {
+ // two halves per layer: the top auth node alone is half of the tree's leaves, the rest the other half
+ tasks.add(authTask(params, skSeed, pkSeed, j, tree[j], leaf[j], hPrime - 1, hPrime, auth));
+ tasks.add(authTask(params, skSeed, pkSeed, j, tree[j], leaf[j], 0, hPrime - 1, auth));
+ }
+ final List> done = pool().invokeAll(tasks);
+ final Object[] fors = (Object[]) done.get(0).get();
+ for (int i = 1; i < done.size(); i++) {
+ done.get(i).get(); // surfaces a failed task, which sends the caller to the original code
+ }
+ for (int j = 0; j < d; j++) {
+ for (int h = 0; h < hPrime; h++) {
+ if (auth[j][h] == null) {
+ throw new IllegalStateException("auth path of layer " + j + " height " + h + " missing");
+ }
+ }
+ }
+ final SIG_FORS[] sigFors = (SIG_FORS[]) fors[0];
+ final byte[] pkFors = (byte[]) fors[1];
+
+ // the hypertree chain, in order: each layer's WOTS+ signs the root of the layer below (FORS for layer 0)
+ final HT ht = new HT(engine, skSeed, pkSeed, true);
+ byte[] m = pkFors;
+ final byte[][] htParts = new byte[d][];
+ for (int j = 0; j < d; j++) {
+ final ADRS adrs = new ADRS();
+ adrs.setLayerAddress(j);
+ adrs.setTreeAddress(tree[j]);
+ final ADRS wotsAdrs = new ADRS(adrs);
+ wotsAdrs.setTypeAndClear(ADRS.WOTS_HASH);
+ wotsAdrs.setKeyPairAddress(leaf[j]);
+ final SIG_XMSS xmss = new SIG_XMSS(ht.wots.sign(m, skSeed, pkSeed, wotsAdrs), auth[j]);
+ htParts[j] = Arrays.concatenate(xmss.sig, Arrays.concatenate(xmss.auth));
+ if (j < d - 1) {
+ m = ht.xmss_pkFromSig(leaf[j], xmss, m, pkSeed, adrs);
+ }
+ }
+
+ final byte[][] parts = new byte[sigFors.length + 2][];
+ parts[0] = r;
+ for (int i = 0; i != sigFors.length; i++) {
+ parts[1 + i] = Arrays.concatenate(sigFors[i].sk, Arrays.concatenate(sigFors[i].authPath));
+ }
+ parts[parts.length - 1] = Arrays.concatenate(htParts);
+ return Arrays.concatenate(parts);
+ }
+
+ private static Callable