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

+ * + *

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 forsTask( + final SLHDSAParameters params, + final byte[] skSeed, + final byte[] pkSeed, + final byte[] mHash, + final long idxTree, + final int idxLeaf) { + return () -> { + TASKS_RUN.incrementAndGet(); + final SLHDSAEngine e = params.getEngine(); + e.init(pkSeed); + final Fors fors = new Fors(e); + ADRS adrs = new ADRS(); + adrs.setTypeAndClear(ADRS.FORS_TREE); + adrs.setTreeAddress(idxTree); + adrs.setKeyPairAddress(idxLeaf); + final SIG_FORS[] sigFors = fors.sign(mHash, skSeed, pkSeed, adrs); + adrs = new ADRS(); + adrs.setTypeAndClear(ADRS.FORS_TREE); + adrs.setTreeAddress(idxTree); + adrs.setKeyPairAddress(idxLeaf); + return new Object[] {sigFors, fors.pkFromSig(sigFors, mHash, pkSeed, adrs)}; + }; + } + + // the auth-path loop of HT.xmss_sign, for heights [fromHeight, toHeight) of one layer's tree + private static Callable authTask( + final SLHDSAParameters params, + final byte[] skSeed, + final byte[] pkSeed, + final int layer, + final long treeAddress, + final int idx, + final int fromHeight, + final int toHeight, + final byte[][][] auth) { + return () -> { + TASKS_RUN.incrementAndGet(); + final SLHDSAEngine e = params.getEngine(); + e.init(pkSeed); + final HT ht = new HT(e, skSeed, pkSeed, true); + final ADRS a = new ADRS(); + a.setTypeAndClear(ADRS.TREE); + a.setLayerAddress(layer); + a.setTreeAddress(treeAddress); + for (int j = fromHeight; j < toHeight; j++) { + final int k = (idx >>> j) ^ 1; + auth[layer][j] = ht.treehash(skSeed, k << j, j, pkSeed, a); + } + return null; + }; + } +} diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG.java index fb6d817..d9a4c3c 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG.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/SIG_FORS.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_FORS.java index 7c00647..5d3c5dc 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_FORS.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_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/SIG_XMSS.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_XMSS.java index f77ffaf..0d64d2a 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_XMSS.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_XMSS.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/SK.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SK.java index 8040421..4ea009d 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SK.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SK.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/SLHDSAEngine.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngine.java index bf7b400..69fcce3 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngine.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngine.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/SLHDSAEngineProvider.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngineProvider.java index 791705d..b15b4c7 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngineProvider.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngineProvider.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/SLHDSAKeyGenerationParameters.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyGenerationParameters.java index b3daec9..12b23ac 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyGenerationParameters.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyGenerationParameters.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/SLHDSAKeyPairGenerator.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyPairGenerator.java index 58a32e8..32879f8 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyPairGenerator.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyPairGenerator.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/SLHDSAKeyParameters.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyParameters.java index 2999c4c..d13985c 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyParameters.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyParameters.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/SLHDSAParameters.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAParameters.java index 4a5e422..1d14c94 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAParameters.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAParameters.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/SLHDSAPrivateKeyParameters.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPrivateKeyParameters.java index da9c060..64e4263 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPrivateKeyParameters.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPrivateKeyParameters.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/SLHDSAPublicKeyParameters.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPublicKeyParameters.java index 544156c..5492012 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPublicKeyParameters.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPublicKeyParameters.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/SLHDSASigner.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.java index 91fdef1..720e4f2 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.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; @@ -191,6 +193,13 @@ public class SLHDSASigner byte[] optRand) { // TODO Check init via privKey != null + // AERE (D-337, 2026-09-26): the same signature, bit for bit, with the message-independent work on + // the free cores (ParallelSigning). null = not taken, and the original code below runs unchanged. + byte[] aere = ParallelSigning.trySign(privKey, msgPrefix, msg, optRand); + if (aere != null) + { + return aere; + } SLHDSAEngine engine = privKey.getParameters().getEngine(); engine.init(privKey.pk.seed); diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java index fe1a94e..47600d1 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.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/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTagsTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTagsTest.java new file mode 100644 index 0000000..f171b39 --- /dev/null +++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTagsTest.java @@ -0,0 +1,235 @@ +/* + * 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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.plugin.data.AddedBlockContext.EventType; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; + +import org.apache.tuweni.bytes.Bytes; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * AERE D-412 Rule B1: finalized/safe follow the last certified anchor's parent, monotonically, and a + * forged between-anchor block can never be reported final. Anchors every 32 from 96 (K=3); the + * certificate predicate is a set of heights that "carry" one, so a planted defect in the rule (an + * anchor without certificate moving the tag, a lower height moving it back, a non-anchor moving it) + * turns exactly one of these red. + */ +class AerePqFinalityTagsTest { + + private static final long ANCHOR_BLOCK = 96L; + private static final int INTERVAL = 32; + + private PqAnchorConfig config; + private MutableBlockchain blockchain; + private final Map byNumber = new HashMap<>(); + private final Map byHash = new HashMap<>(); + private final Set certified = new HashSet<>(); + + @BeforeEach + void setUp() { + config = + new PqAnchorConfig(2800L, ANCHOR_BLOCK, Map.of(ANCHOR_BLOCK, 3), OptionalInt.empty(), false) + .withAnchorInterval(OptionalInt.of(INTERVAL)); + blockchain = mock(MutableBlockchain.class); + // a linear chain 0..200 with deterministic hashes; header(n).parentHash == hash(header(n-1)) + Hash prev = Hash.ZERO; + for (long n = 0; n <= 200; n++) { + final BlockHeader h = new BlockHeaderTestFixture().number(n).parentHash(prev).buildHeader(); + byNumber.put(n, h); + byHash.put(h.getHash(), h); + prev = h.getHash(); + } + when(blockchain.getBlockHeader(anyLong())).thenAnswer(i -> Optional.ofNullable(byNumber.get(i.getArgument(0, Long.class)))); + when(blockchain.getBlockHeader(any(Hash.class))).thenAnswer(i -> Optional.ofNullable(byHash.get(i.getArgument(0, Hash.class)))); + } + + private AerePqFinalityTags tagsWithHead(final long head) { + when(blockchain.getChainHeadHeader()).thenReturn(byNumber.get(head)); + return AerePqFinalityTags.install(config, blockchain, h -> certified.contains(h.getNumber())); + } + + private BlockAddedEvent headAdvanced(final long n) { + final BlockAddedEvent e = mock(BlockAddedEvent.class); + when(e.isNewCanonicalHead()).thenReturn(true); + when(e.getHeader()).thenReturn(byNumber.get(n)); + when(e.getEventType()).thenReturn(EventType.HEAD_ADVANCED); + return e; + } + + private BlockAddedEvent reorgTo(final long n, final long commonAncestor) { + final BlockAddedEvent e = mock(BlockAddedEvent.class); + when(e.isNewCanonicalHead()).thenReturn(true); + when(e.getHeader()).thenReturn(byNumber.get(n)); + when(e.getEventType()).thenReturn(EventType.CHAIN_REORG); + when(e.getCommonAncestorHash()).thenReturn(byNumber.get(commonAncestor).getHash()); + return e; + } + + @Test + void theConfigurationUnderTestHasAnchorsEveryThirtyTwoFromNinetySix() { + assertThat(config.anchorAppliesAt(96)).isTrue(); + assertThat(config.anchorAppliesAt(128)).isTrue(); + assertThat(config.anchorAppliesAt(129)).isFalse(); + assertThat(config.anchorAppliesAt(95)).isFalse(); + } + + @Test + void startupScanFinalizesTheParentOfTheLastCertifiedAnchorBelowTheHead() { + certified.add(96L); + certified.add(128L); + final AerePqFinalityTags tags = tagsWithHead(140); + verify(blockchain, times(1)).setFinalized(byNumber.get(127L).getHash()); + verify(blockchain, times(1)).setSafeBlock(byNumber.get(127L).getHash()); + assertThat(tags.finalizedNumber()).isEqualTo(127L); + } + + @Test + void startupWithNoCertifiedAnchorInTwoIntervalsSetsNothing() { + final AerePqFinalityTags tags = tagsWithHead(140); // 96 and 128 are anchor heights, none certified + verify(blockchain, never()).setFinalized(any()); + assertThat(tags.finalizedNumber()).isEqualTo(-1L); + } + + @Test + void aCertifiedAnchorAtTheHeadFinalizesItsParent() { + certified.add(96L); + final AerePqFinalityTags tags = tagsWithHead(100); // finalized 95 + certified.add(128L); + tags.onBlockAdded(headAdvanced(128)); + verify(blockchain).setFinalized(byNumber.get(127L).getHash()); + verify(blockchain).setSafeBlock(byNumber.get(127L).getHash()); + assertThat(tags.finalizedNumber()).isEqualTo(127L); + } + + @Test + void aBetweenAnchorHeadNeverMovesTheTags_thisIsWhatKeepsAForgedTailOutOfFinalized() { + certified.add(96L); + final AerePqFinalityTags tags = tagsWithHead(100); + for (long n = 101; n < 128; n++) { + certified.add(n); // even if a between-anchor header somehow "carried" a certificate + tags.onBlockAdded(headAdvanced(n)); + } + verify(blockchain, times(1)).setFinalized(any()); // only the startup one (95) + assertThat(tags.finalizedNumber()).isEqualTo(95L); + } + + @Test + void anAnchorHeightWithoutACertificateDoesNotMoveTheTags() { + certified.add(96L); + final AerePqFinalityTags tags = tagsWithHead(100); + tags.onBlockAdded(headAdvanced(128)); // 128 not certified + verify(blockchain, times(1)).setFinalized(any()); + assertThat(tags.finalizedNumber()).isEqualTo(95L); + } + + @Test + void theTagsAreMonotonic_aLowerCertifiedAnchorNeverMovesThemBack() { + certified.add(96L); + certified.add(128L); + final AerePqFinalityTags tags = tagsWithHead(140); // finalized 127 + tags.onBlockAdded(headAdvanced(96)); // certified, but lower + verify(blockchain, times(1)).setFinalized(any()); + assertThat(tags.finalizedNumber()).isEqualTo(127L); + } + + @Test + void aReorgBelowTheFinalizedBlockIsNotFollowedBackwards() { + certified.add(96L); + certified.add(128L); + final AerePqFinalityTags tags = tagsWithHead(140); // finalized 127 + tags.onBlockAdded(reorgTo(130, 100)); // common ancestor 100 < 127: logged, not moved + verify(blockchain, times(1)).setFinalized(any()); + assertThat(tags.finalizedNumber()).isEqualTo(127L); + } + + @Test + void aNonCanonicalEventIsIgnored() { + certified.add(96L); + certified.add(128L); + final AerePqFinalityTags tags = tagsWithHead(100); // finalized 95 + final BlockAddedEvent fork = mock(BlockAddedEvent.class); + when(fork.isNewCanonicalHead()).thenReturn(false); + when(fork.getHeader()).thenReturn(byNumber.get(128L)); + tags.onBlockAdded(fork); + assertThat(tags.finalizedNumber()).isEqualTo(95L); + } + + @Test + void headersAreLinkedByParentHashInTheFixture() { + assertThat(byNumber.get(128L).getParentHash()).isEqualTo(byNumber.get(127L).getHash()); + assertThat(byNumber.get(127L).getHash().size()).isEqualTo(32); + } + + // --- the certificate predicate: the controller feeds carriesCertificate(decode(header)) into install(), + // so it decides which anchors move the tags. On a stage-5 chain the certificate is the v2 (hybrid) list + // and getFalconSeals() is empty; a predicate that read only the v1 list never fired (rep28098 rehearsal, + // 2026-09-26). These three cases are the control that both fields are read. + + private static BftExtraData extraWith(final List v1, final List v2) { + return new BftExtraData( + Bytes.EMPTY, Collections.emptyList(), Optional.empty(), 0, Collections.emptyList(), v1, v2); + } + + @Test + void carriesCertificate_trueForAV1FalconList() { + assertThat( + AerePqFinalityTags.carriesCertificate( + extraWith(List.of(new FalconSeal(0, Bytes.of(1, 2, 3))), Collections.emptyList()))) + .isTrue(); + } + + @Test + void carriesCertificate_trueForAV2HybridCertificate_thisIsTheStage5AnchorThatWasBeingMissed() { + assertThat( + AerePqFinalityTags.carriesCertificate( + extraWith( + Collections.emptyList(), + List.of(new SchemeSeal((byte) 1, 0, Bytes.of(1, 2, 3)))))) + .isTrue(); + } + + @Test + void carriesCertificate_falseWhenNeitherListIsPresent() { + assertThat( + AerePqFinalityTags.carriesCertificate( + extraWith(Collections.emptyList(), Collections.emptyList()))) + .isFalse(); + } +} diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java index 87bca04..e2e7be5 100644 --- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java +++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java @@ -134,7 +134,7 @@ class HybridSignerRegistryTest { @Test void unrecognisedEntryRefuses() { final Properties p = sanatos(); - p.setProperty("1.cheie", "0x1234"); // aproape corect, dar nu e nici addr nici key. + p.setProperty("1.cheie", "0x1234"); // almost right, but it is neither addr nor key. assertThatThrownBy(() -> HybridSignerRegistry.fromProperties(p, "test")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("unrecognised"); @@ -149,7 +149,7 @@ class HybridSignerRegistryTest { final HybridSignerRegistry b = HybridSignerRegistry.fromProperties(p, "b"); assertThat(a.canonicalHash()).isEqualTo(b.canonicalHash()); // determinist - // schimb O cheie: hash-ul se misca + // change ONE key: the hash moves final Properties altKey = sanatos(); altKey.setProperty( "2.key." + SLHDSA, @@ -157,7 +157,7 @@ class HybridSignerRegistryTest { assertThat(HybridSignerRegistry.fromProperties(altKey, "c").canonicalHash()) .isNotEqualTo(a.canonicalHash()); - // scot o schema de la un validator: hash-ul se misca + // remove one scheme from a validator: the hash moves final Properties altScheme = sanatos(); altScheme.remove("2.key." + SLHDSA); assertThat(HybridSignerRegistry.fromProperties(altScheme, "d").canonicalHash()) @@ -200,7 +200,7 @@ class HybridSignerRegistryTest { p.setProperty("1.key." + FALCON, Bytes.wrap(f1.publicRegistryForm()).toHexString()); final HybridSignerRegistry reg = HybridSignerRegistry.fromProperties(p, "test"); - // certificatul hibrid: validatorul 0 cu amandoua schemele, 1 doar Falcon + // the hybrid certificate: validator 0 with both schemes, 1 with Falcon only final java.util.List cert = java.util.List.of( new SchemeSeal((byte) 0x01, 0, Bytes.wrap( 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 index e4218b1..41f03ce 100644 --- 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 @@ -77,7 +77,7 @@ class PqSchemeScheduleTest { 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 + // every index gets at least one key; per-scheme coverage is controlled below if (i < falconi) { p.setProperty(i + ".key." + FALCON, Bytes.wrap(SealSchemes.FALCON_512.generate(random).publicRegistryForm()).toHexString()); @@ -98,7 +98,7 @@ class PqSchemeScheduleTest { 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 + // the DANGEROUS step is the LAST: the hybrid requires SLH-DSA with coverage 1 < K=3 final PqSchemeSchedule orar = PqSchemeSchedule.parse("100:falcon-512,999999:falcon-512+slh-dsa-128s"); final var refusal = orar.firstUnsatisfied(reg, 3); @@ -122,20 +122,20 @@ class PqSchemeScheduleTest { @Test void perSchemeIntervalsParseAndOnlyBiteOnTheirOwnGrid() { - // Falcon la fiecare al 32-lea bloc, SLH-DSA doar la fiecare al 128-lea, grilele numarate de la H0=64 + // Falcon every 32nd block, SLH-DSA only every 128th, grids counted from H0=64 final PqSchemeSchedule orar = PqSchemeSchedule.parse("1000:" + FALCON + "/32+" + SLHDSA_CANONIC + "/128"); assertThat(orar.hasIntervals()).isTrue(); // schemesAt ramane setul intreg: cheile si armabilitatea privesc toate schemele assertThat(orar.schemesAt(1000)).containsExactlyInAnyOrder(FALCON, SLHDSA_CANONIC); - // 64 + 32k dar nu 64 + 128k: doar Falcon + // 64 + 32k but not 64 + 128k: Falcon only assertThat(orar.schemesRequiredForAnchor(1056, 64)).containsExactly(FALCON); // 1056-64 = 992 = 31*32 - assertThat(orar.schemesRequiredForAnchor(1120, 64)).containsExactly(FALCON); // 1056 = 33*32, nu multiplu de 128 + assertThat(orar.schemesRequiredForAnchor(1120, 64)).containsExactly(FALCON); // 1056 = 33*32, not a multiple of 128 // 64 + 128k: amandoua assertThat(orar.schemesRequiredForAnchor(1216, 64)).containsExactlyInAnyOrder(FALCON, SLHDSA_CANONIC); // 1152 = 9*128 // sub prima treapta (parintele 999 e sub 1000): nimic assertThat(orar.schemesRequiredForAnchor(1000, 64)).isEmpty(); - // pe granita: treapta se citeste la PARINTE (1000), ancora e 1001... care nu e pe grila 32 de la 64 + // at the boundary: the step is read at the PARENT (1000), the anchor is 1001... which is not on the 32 grid from 64 assertThat(orar.schemesRequiredForAnchor(1001, 64)).isEmpty(); } diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SlhDsaParallelSigningTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SlhDsaParallelSigningTest.java new file mode 100644 index 0000000..d4f0726 --- /dev/null +++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SlhDsaParallelSigningTest.java @@ -0,0 +1,137 @@ +/* + * 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.slhdsa; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; + +import java.nio.charset.StandardCharsets; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.hyperledger.besu.crypto.SecureRandomProvider; +import org.junit.jupiter.api.Test; + +/** + * D-337 (2026-09-26): the parallel SLH-DSA signer must be the SAME signature as the Bouncy Castle + * original, bit for bit, and must actually be the path taken. Otherwise the anchor certificate would + * carry a different SLH-DSA seal than the second client and the public verifier expect, or the + * speed-up would be claimed while the sequential code still ran. Pinned: deterministic signing over + * several keys and message lengths equals the original; every signature ran exactly 1 + 2D tasks on + * the pool; the kill switch falls back to the original code with the same bytes and no task; and a + * randomised signature from the parallel path verifies under the original. + */ +class SlhDsaParallelSigningTest { + + private static final org.bouncycastle.pqc.crypto.slhdsa.SLHDSAParameters ORIGINAL = + org.bouncycastle.pqc.crypto.slhdsa.SLHDSAParameters.sha2_128s; + + private static AsymmetricCipherKeyPair keyPair() { + final SLHDSAKeyPairGenerator gen = new SLHDSAKeyPairGenerator(); + gen.init( + new SLHDSAKeyGenerationParameters( + SecureRandomProvider.createSecureRandom(), SLHDSAParameters.sha2_128s)); + return gen.generateKeyPair(); + } + + private static org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner original( + final AsymmetricCipherKeyPair kp, final boolean forSigning) { + final org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner s = + new org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner(); + if (forSigning) { + s.init( + true, + new org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPrivateKeyParameters( + ORIGINAL, ((SLHDSAPrivateKeyParameters) kp.getPrivate()).getEncoded())); + } else { + s.init( + false, + new org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPublicKeyParameters( + ORIGINAL, ((SLHDSAPublicKeyParameters) kp.getPublic()).getEncoded())); + } + return s; + } + + private static byte[] ours(final AsymmetricCipherKeyPair kp, final byte[] msg) { + final SLHDSASigner s = new SLHDSASigner(); + s.init(true, kp.getPrivate()); + return s.generateSignature(msg); + } + + @Test + void parallelSignaturesAreBitIdenticalToTheOriginalAndTakeTheParallelPath() throws Exception { + assumeThat(ParallelSigning.enabled()).as("needs more than one core").isTrue(); + final int perSignature = ParallelSigning.tasksPerSignature(SLHDSAParameters.sha2_128s); + assertThat(perSignature).isEqualTo(15); // FORS + two halves of each of the 7 hypertree layers + final byte[][] messages = { + new byte[0], + "a".getBytes(StandardCharsets.UTF_8), + new byte[32], + "anchor parent 20333167 on chain 2800, a longer message".getBytes(StandardCharsets.UTF_8) + }; + int signatures = 0; + for (int key = 0; key < 3; key++) { + final AsymmetricCipherKeyPair kp = keyPair(); + for (final byte[] msg : messages) { + final int before = ParallelSigning.tasksRun(); + final byte[] fast = ours(kp, msg); + final int ran = ParallelSigning.tasksRun() - before; + assertThat(ran).as("tasks run on the pool for one signature").isEqualTo(perSignature); + assertThat(fast).hasSize(7856); + assertThat(fast).isEqualTo(original(kp, true).generateSignature(msg)); + assertThat(original(kp, false).verifySignature(msg, fast)).isTrue(); + signatures++; + } + } + assertThat(signatures).isEqualTo(12); + } + + @Test + void theKillSwitchRunsTheOriginalCodeWithTheSameBytes() throws Exception { + final AsymmetricCipherKeyPair kp = keyPair(); + final byte[] msg = "kill switch".getBytes(StandardCharsets.UTF_8); + final String before = System.getProperty(ParallelSigning.SWITCH_PROPERTY); + System.setProperty(ParallelSigning.SWITCH_PROPERTY, "false"); + try { + assertThat(ParallelSigning.enabled()).isFalse(); + final int tasksBefore = ParallelSigning.tasksRun(); + final byte[] sig = ours(kp, msg); + assertThat(ParallelSigning.tasksRun()).as("no task on the pool").isEqualTo(tasksBefore); + assertThat(sig).isEqualTo(original(kp, true).generateSignature(msg)); + } finally { + if (before == null) { + System.clearProperty(ParallelSigning.SWITCH_PROPERTY); + } else { + System.setProperty(ParallelSigning.SWITCH_PROPERTY, before); + } + } + } + + @Test + void aRandomisedParallelSignatureVerifiesUnderTheOriginalAndATamperedOneDoesNot() + throws Exception { + assumeThat(ParallelSigning.enabled()).as("needs more than one core").isTrue(); + final AsymmetricCipherKeyPair kp = keyPair(); + final byte[] msg = "randomised".getBytes(StandardCharsets.UTF_8); + final SLHDSASigner s = new SLHDSASigner(); + s.init(true, new ParametersWithRandom(kp.getPrivate(), SecureRandomProvider.createSecureRandom())); + final int before = ParallelSigning.tasksRun(); + final byte[] sig = s.generateSignature(msg); + assertThat(ParallelSigning.tasksRun() - before).isEqualTo(15); + assertThat(original(kp, false).verifySignature(msg, sig)).isTrue(); + final byte[] bad = sig.clone(); + bad[4000] ^= 0x01; + assertThat(original(kp, false).verifySignature(msg, bad)).isFalse(); + } +} diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java index 18437e3..236b6cb 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java @@ -142,10 +142,10 @@ public class QbftMessageTransmitter { /** * Multicast a prepare carrying an OPTIONAL post-quantum seal of this node. * - *

Sigiliul vine GATA CALCULAT de la apelant, si asta nu e comoditate: semnaturile Falcon sunt + *

The seal arrives ALREADY COMPUTED from the caller, and that is not a convenience: Falcon signatures are * 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. + * valid and DIFFERENT for the same round. It is computed once, above, and passed through both. * * @param roundIdentifier the round identifier * @param digest the digest diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java index 11426d6..24777b5 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java @@ -142,10 +142,10 @@ public class MessageFactory { /** * Create a Prepare carrying an OPTIONAL post-quantum seal of its author. * - *

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

AERE PQ (2026-08-28), step 1: the wire can carry the seal, and nothing emits it yet - + * every call today goes through the seal-less variant above. As with commit, the author's ECDSA + * signature covers the WHOLE payload, and so the seal too, which binds the claimed index to + * the identity of whoever sends the message. * * @param roundIdentifier the round identifier * @param digest the digest diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java index e5f1a59..d4f6e2d 100644 --- a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java +++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java @@ -106,6 +106,19 @@ public class QbftExtraDataCodec extends BftExtraDataCodec { rlpInput.enterList(); // This accounts for the "root node" which contains BFT data items. final Bytes vanityData = rlpInput.readBytes(); + // AERE-D415 (2026-09-25): the vanity field is exactly 32 bytes at EVERY height, not only where + // the anchor rule looks (PqAnchorDigestRule checks it at anchor heights alone). The second + // client's decoder has required 32 from the start, so every header of chains 2800 and 28001, + // both geneses included, satisfies it; the differential fuzzer of the stage-5 header rule found + // the two clients judging a 0-, 31-, 33- or 64-byte vanity between anchors differently (this + // codec admitted it, the second client refused it). One rule, in the codec, for both clients. + if (vanityData.size() != EXTRA_VANITY_LENGTH) { + throw new RLPException( + "AERE-D415: QBFT vanity must be exactly " + + EXTRA_VANITY_LENGTH + + " bytes, the header carries " + + vanityData.size()); + } final List

validators = rlpInput.readList(Address::readFrom); final Optional vote; diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java index c877c12..e292937 100644 --- a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java +++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java @@ -20,9 +20,11 @@ import org.hyperledger.besu.consensus.common.bft.AerePqIdentity; import org.hyperledger.besu.consensus.common.bft.BftBlockHashing; import org.hyperledger.besu.consensus.common.bft.BftExtraData; import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec; +import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.FalconSeal; import org.hyperledger.besu.consensus.common.bft.PqAnchor; import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry; +import org.hyperledger.besu.consensus.common.bft.blockcreation.BftProposerSelector; import org.hyperledger.besu.consensus.common.bft.headervalidationrules.BftCommitSealsValidationRule; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.datatypes.Address; @@ -30,6 +32,7 @@ import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.mainnet.AttachedBlockHeaderValidationRule; +import java.util.Collection; import java.util.Optional; import org.apache.tuweni.bytes.Bytes32; @@ -44,6 +47,18 @@ import org.slf4j.LoggerFactory; * verifies over the PROPOSAL message recomputed from the stored header (chain id, height, the round in * element 3, the proposal digest, i.e. the committed-seal hash of the header). Finality stays with the * anchors (AIP-22 section 4); this rule is what admits a header between them. + * + *

AERE D-406 (2026-09-25, measured on testnet 28001): a block PREPARED in one round and not + * committed before its timer is RE-PROPOSED unchanged in a later round r by the proposer of round r + * (QBFT's locked value; only element 3 changes, the coinbase stays the builder's), and every node seals + * the header with the accepted proposal's seal, which is the round-r proposer's. Requiring the coinbase + * refused every such header on every node: block 3,314,873 stood 128 s (rounds 1 to 4 refused with + * "a seal cannot vouch for another proposer", round 5 was the builder's own turn again, N = 5); at + * N = 10 the same wait is ten rounds, and with the builder down after the prepare it does not end. So + * the seal's id must be the coinbase OR, at a round above zero, the proposer of the round in element 3 + * as upstream's proposer selection computes it from the parent's proposer (its coinbase) and the + * header's own validator list (the set after the parent, which BftValidatorsValidationRule enforces). + * Every header the old rule admitted is admitted unchanged; the new branch only runs where it refused. */ public class PqHeaderSealRule implements AttachedBlockHeaderValidationRule { @@ -91,18 +106,86 @@ public class PqHeaderSealRule implements AttachedBlockHeaderValidationRule { if (!AerePqHeaderSeal.active(header.getNumber())) { return belowHdr.validate(header, parent, protocolContext); } - final Optional r = refusal(header); + final Optional r = refusal(header, parent); r.ifPresent(LOG::info); return r.isEmpty(); } + /** + * Why a header at or above H_hdr is refused, if it is, judged WITHOUT its parent: only a seal of the + * coinbase's index can be admitted (a re-proposed header needs the parent, {@link #refusal(BlockHeader, + * BlockHeader)}). + * + * @param header the header + * @return the refusal, or empty + */ + public Optional refusal(final BlockHeader header) { + return refusal(header, null); + } + + /** + * AERE D-406: the proposer of the round in element 3, as upstream's selection computes it (changeEachBlock, + * as QBFT), from the parent's proposer and the header's validator list. Empty at round 0, without the + * parent, or when the list cannot seed a selection. + * + * @param h the height + * @param round the round in element 3 + * @param parent the parent header, or null + * @param validators the header's validator list + * @return the proposer of that round, or empty + */ + static Optional

proposerOfRound( + final long h, final int round, final BlockHeader parent, final Collection
validators) { + if (round <= 0 || round > MAX_ROUND || parent == null || validators == null || validators.isEmpty()) { + return Optional.empty(); + } + try { + return Optional.of( + BftProposerSelector.selectProposerForRound( + new ConsensusRoundIdentifier(h, round), + parent.getCoinbase(), + validators, + true)); + } catch (final RuntimeException e) { + return Optional.empty(); + } + } + + /** + * AERE D-406: the highest round element 3 may carry from H_hdr. No honest round gets near it (the QBFT round timer doubles each + * round), and it keeps the rotation arithmetic, which upstream does in int, in range. The second client applies the same bound. + */ + static final int MAX_ROUND = 1024; + + /** + * AERE D-406: whether {@code coinbase} is the proposer of some round 0..round at this height, i.e. could have BUILT the block + * (a block is built by the proposer of the round it is first proposed in, and a re-proposal keeps its coinbase). The rotation is + * periodic in n, so rounds 0..min(round, n - 1) cover the set. The second client has required exactly this since D-331. + */ + static boolean builtByAProposerUpTo( + final long h, final int round, final BlockHeader parent, final Collection
validators, final Address coinbase) { + final int last = Math.min(round, validators.size() - 1); + for (int r = 0; r <= last; r++) { + try { + if (BftProposerSelector.selectProposerForRound(new ConsensusRoundIdentifier(h, r), parent.getCoinbase(), validators, true) + .equals(coinbase)) { + return true; + } + } catch (final RuntimeException e) { + return false; + } + } + return false; + } + /** * Why a header at or above H_hdr is refused, if it is. * * @param header the header + * @param parent its parent, or null (then only the coinbase's seal can be admitted) * @return the refusal, or empty */ - public Optional refusal(final BlockHeader header) { + public Optional refusal(final BlockHeader header, final BlockHeader parent) { final long h = header.getNumber(); final BftExtraData ed; try { @@ -118,6 +201,17 @@ public class PqHeaderSealRule implements AttachedBlockHeaderValidationRule { if (ed.getPqProposerSeal().isEmpty()) { return Optional.of("AERE PQ HEADER: block " + h + " carries no proposer seal (element 6) from H_hdr"); } + // AERE D-406: the round bound, and with the parent, the builder: the coinbase is the proposer of some round up to element 3 + if (ed.getRound() < 0 || ed.getRound() > MAX_ROUND) { + return Optional.of("AERE PQ HEADER: block " + h + " round " + ed.getRound() + " in element 3 is outside 0.." + MAX_ROUND); + } + if (parent != null + && !ed.getValidators().isEmpty() + && !builtByAProposerUpTo(h, ed.getRound(), parent, ed.getValidators(), header.getCoinbase())) { + return Optional.of( + "AERE PQ HEADER: block " + h + " coinbase " + header.getCoinbase() + " is not the proposer of any round 0.." + + ed.getRound() + " at this height, so it did not build the block"); + } final FalconSeal ps = ed.getPqProposerSeal().get(); final Optional
id = AerePqIdentity.v3IdAt(h, ps.getValidatorIndex()); if (id.isEmpty()) { @@ -125,9 +219,19 @@ public class PqHeaderSealRule implements AttachedBlockHeaderValidationRule { "AERE PQ HEADER: block " + h + " proposer seal index " + ps.getValidatorIndex() + " names no row of the v3 epoch in force"); } if (!id.get().equals(header.getCoinbase())) { - return Optional.of( - "AERE PQ HEADER: block " + h + " proposer seal of index " + ps.getValidatorIndex() + " (id " + id.get() - + ") but coinbase " + header.getCoinbase() + " - a seal cannot vouch for another proposer"); + // AERE D-406: a re-proposed block carries the seal of the proposer of the round it was committed in + final Optional
roundProposer = proposerOfRound(h, ed.getRound(), parent, ed.getValidators()); + if (roundProposer.isEmpty() || !roundProposer.get().equals(id.get())) { + return Optional.of( + "AERE PQ HEADER: block " + h + " proposer seal of index " + ps.getValidatorIndex() + " (id " + id.get() + + ") but coinbase " + header.getCoinbase() + + (ed.getRound() > 0 + ? roundProposer + .map(p -> " and the proposer of round " + ed.getRound() + " is " + p) + .orElse(" and the proposer of round " + ed.getRound() + " is not known without the parent") + : "") + + " - a seal cannot vouch for another proposer"); + } } final Bytes32 message; try { diff --git a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java index e9f3236..2a928bd 100644 --- a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java +++ b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java @@ -38,6 +38,7 @@ import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; import org.hyperledger.besu.ethereum.core.Util; import org.hyperledger.besu.ethereum.mainnet.AttachedBlockHeaderValidationRule; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; +import org.hyperledger.besu.ethereum.rlp.RLPException; import java.util.ArrayList; import java.util.Collections; @@ -215,6 +216,256 @@ public class PqHeaderSealStage5Test { assertThat(rule.refusal(header(H_HDR, ids.get(0), extra(1, List.of(), sealOver(r1, 1, 0, 0))))).isEmpty(); } + // ---- AERE D-406 (2026-09-25): a block re-proposed at round r carries the seal of round r's proposer ---- + + /** The validator list as a real header stores it: ascending. */ + private List
sortedIds() { + final List
s = new ArrayList<>(ids); + Collections.sort(s); + return s; + } + + /** + * The proposer of round r written HERE, independently of the rule (which calls upstream's selector): the + * existing-proposer case is list[(indexOf(prev) + 1 + r) mod n], the missing-proposer case seeds from the first + * validator strictly above prev (the first if none) with no +1. + */ + private static Address expectedProposer(final List
sorted, final Address prev, final int r) { + final int n = sorted.size(); + final int i = sorted.indexOf(prev); + if (i >= 0) { + return sorted.get((i + 1 + r) % n); + } + int seed = 0; + for (int k = 0; k < n; k++) { + if (sorted.get(k).compareTo(prev) > 0) { + seed = k; + break; + } + } + return sorted.get((seed + r) % n); + } + + private BlockHeader parentWithCoinbase(final long h, final Address coinbase) { + return new BlockHeaderTestFixture().number(h - 1).coinbase(coinbase).buildHeader(); + } + + /** A header at h, round r, built by {@code coinbase}, sealed as index {@code index} by key {@code signer}. */ + private BlockHeader reproposed(final long h, final int round, final Address coinbase, final int index, final int signer) { + final List
sorted = sortedIds(); + final BftExtraData bare = + new BftExtraData( + Bytes.wrap(new byte[32]), List.of(), Optional.empty(), round, sorted, Collections.emptyList(), + Collections.emptyList(), Optional.empty()); + final BlockHeader unsealed = header(h, coinbase, bare); + final BftExtraData sealed = + new BftExtraData( + Bytes.wrap(new byte[32]), List.of(), Optional.empty(), round, sorted, Collections.emptyList(), + Collections.emptyList(), sealOver(unsealed, round, index, signer)); + return header(h, coinbase, sealed); + } + + @Test + public void aReProposedHeaderSealedByTheProposerOfItsRoundIsAdmittedOnlyWithItsParent() { + final List
sorted = sortedIds(); + final long h = H_HDR + 5; + int judged = 0; + for (int p = 0; p < N; p++) { + final Address prev = sorted.get(p); + final Address builder = expectedProposer(sorted, prev, 0); + for (int r = 1; r < 2 * N; r++) { + final Address roundProposer = expectedProposer(sorted, prev, r); + if (roundProposer.equals(builder)) { + continue; + } + final int idx = ids.indexOf(roundProposer); + final BlockHeader hd = reproposed(h, r, builder, idx, idx); + final BlockHeader parent = parentWithCoinbase(h, prev); + assertThat(rule.refusal(hd, parent)).as("prev %s round %s", p, r).isEmpty(); + assertThat(rule.validate(hd, parent, null)).as("prev %s round %s", p, r).isTrue(); + // without the parent the round's proposer is not known: refused, and the reason says why + assertThat(rule.refusal(hd)).get().asString() + .contains("cannot vouch").contains("not known without the parent"); + judged++; + } + } + // every (prev, round) pair whose round proposer differs from the builder was judged: 4 x (7 - 1) at N = 4 + assertThat(judged).isEqualTo(N * (2 * N - 1 - 1)); + } + + @Test + public void aReProposedHeaderSealedByAThirdValidatorIsRefusedAndTheReasonNamesTheRoundsProposer() { + final List
sorted = sortedIds(); + final long h = H_HDR + 6; + final Address prev = sorted.get(0); + final Address builder = expectedProposer(sorted, prev, 0); + final int r = 1; + final Address roundProposer = expectedProposer(sorted, prev, r); + Address third = null; + for (final Address a : sorted) { + if (!a.equals(builder) && !a.equals(roundProposer)) { + third = a; + break; + } + } + final int idx = ids.indexOf(third); + final Optional why = rule.refusal(reproposed(h, r, builder, idx, idx), parentWithCoinbase(h, prev)); + assertThat(why).get().asString() + .contains("cannot vouch") + .contains("the proposer of round " + r + " is " + roundProposer); + // the round proposer's key over ANOTHER round than element 3: the id passes, the signature does not + final int rp = ids.indexOf(roundProposer); + final List
s2 = sortedIds(); + final BlockHeader atRound0 = + header(h, builder, new BftExtraData(Bytes.wrap(new byte[32]), List.of(), Optional.empty(), 0, s2, + Collections.emptyList(), Collections.emptyList(), Optional.empty())); + final BlockHeader wrongRound = + header(h, builder, new BftExtraData(Bytes.wrap(new byte[32]), List.of(), Optional.empty(), r, s2, + Collections.emptyList(), Collections.emptyList(), sealOver(atRound0, 0, rp, rp))); + assertThat(rule.refusal(wrongRound, parentWithCoinbase(h, prev))).get().asString().contains("does not verify"); + } + + @Test + public void theSealMustBeExactlyTheProposerOfRoundRNotOfAnEarlierRound() { + // found by the adversarial review of 2026-09-25: an implementation admitting ANY P(r') with r' <= r passed every probe above + final List
sorted = sortedIds(); + final long h = H_HDR + 9; + final Address prev = sorted.get(0); + final Address builder = expectedProposer(sorted, prev, 0); + final Address p1 = expectedProposer(sorted, prev, 1); + final Address p2 = expectedProposer(sorted, prev, 2); + assertThat(p1).isNotEqualTo(p2); + final int i1 = ids.indexOf(p1); + assertThat(rule.refusal(reproposed(h, 2, builder, i1, i1), parentWithCoinbase(h, prev))).get().asString() + .contains("cannot vouch").contains("the proposer of round 2 is " + p2); + final int i2 = ids.indexOf(p2); + assertThat(rule.refusal(reproposed(h, 2, builder, i2, i2), parentWithCoinbase(h, prev))).isEmpty(); + } + + @Test + public void withTheParentTheCoinbaseMustBeTheProposerOfSomeRoundUpToElement3() { + // the second client has refused this since D-331; the Besu-derived rule did not, so the two judged such a header differently + final List
sorted = sortedIds(); + final long h = H_HDR + 10; + final Address prev = sorted.get(0); + final Address p1 = expectedProposer(sorted, prev, 1); + final Address p3 = expectedProposer(sorted, prev, 3); + final int i1 = ids.indexOf(p1); + // round 1, built by P(3) (a proposer of a LATER round), sealed by P(1): the seal is the round's, the builder is impossible + assertThat(rule.refusal(reproposed(h, 1, p3, i1, i1), parentWithCoinbase(h, prev))).get().asString() + .contains("is not the proposer of any round 0..1"); + // and a coinbase-sealed header at round 0 whose coinbase is not P(0) + final int i3 = ids.indexOf(p3); + assertThat(rule.refusal(reproposed(h, 0, p3, i3, i3), parentWithCoinbase(h, prev))).get().asString() + .contains("is not the proposer of any round 0..0"); + // without the parent the builder cannot be judged, and the coinbase's own seal is admitted as before + assertThat(rule.refusal(reproposed(h, 0, p3, i3, i3))).isEmpty(); + } + + @Test + public void theRoundInElement3IsBoundedAndTheBoundIsInclusive() { + final List
sorted = sortedIds(); + final long h = H_HDR + 11; + final Address prev = sorted.get(2); + final int max = PqHeaderSealRule.MAX_ROUND; + // at the bound: a re-proposal sealed by P(max) is admitted (upstream's int arithmetic stays in range) + final Address builder = expectedProposer(sorted, prev, 0); + final Address pm = expectedProposer(sorted, prev, max); + final int im = ids.indexOf(pm); + final int ib = ids.indexOf(builder); + final Address sealer = pm.equals(builder) ? builder : pm; + final int is = sealer.equals(builder) ? ib : im; + assertThat(rule.refusal(reproposed(h, max, builder, is, is), parentWithCoinbase(h, prev))).isEmpty(); + // above it: refused by the bound, with or without the parent, whoever sealed + assertThat(rule.refusal(reproposed(h, max + 1, builder, ib, ib), parentWithCoinbase(h, prev))).get().asString() + .contains("is outside 0.." + max); + assertThat(rule.refusal(reproposed(h, max + 1, builder, ib, ib))).get().asString().contains("is outside 0.." + max); + } + + @Test + public void atRoundZeroOnlyTheCoinbaseSealIsAdmittedEvenWithTheParent() { + final List
sorted = sortedIds(); + final long h = H_HDR + 7; + final Address prev = sorted.get(1); + final Address builder = expectedProposer(sorted, prev, 0); + for (final Address other : sorted) { + if (other.equals(builder)) { + continue; + } + final int idx = ids.indexOf(other); + final Optional why = rule.refusal(reproposed(h, 0, builder, idx, idx), parentWithCoinbase(h, prev)); + assertThat(why).get().asString().contains("cannot vouch").doesNotContain("proposer of round"); + } + final int b = ids.indexOf(builder); + assertThat(rule.refusal(reproposed(h, 0, builder, b, b), parentWithCoinbase(h, prev))).isEmpty(); + } + + @Test + public void whenTheParentsProposerLeftTheSetTheRoundsProposerFollowsUpstreamsMissingProposerSeed() { + final List
sorted = sortedIds(); + final long h = H_HDR + 8; + // a parent proposer below every id (the seed is the first validator) and one above every id (it wraps to the first) + final Address below = Address.fromHexString("0x0000000000000000000000000000000000000001"); + final Address above = Address.fromHexString("0xffffffffffffffffffffffffffffffffffffffff"); + // and one strictly between the second and the third id, so the seed is the third (index 2), not the first + final byte[] mid = sorted.get(1).getBytes().toArrayUnsafe().clone(); + int k = mid.length - 1; + while (k >= 0 && mid[k] == (byte) 0xff) { + mid[k--] = 0; + } + mid[k]++; + final Address middle = Address.wrap(Bytes.wrap(mid)); + assertThat(middle.compareTo(sorted.get(1)) > 0 && middle.compareTo(sorted.get(2)) < 0).isTrue(); + assertThat(expectedProposer(sorted, middle, 0)).isEqualTo(sorted.get(2)); + int judged = 0; + for (final Address prev : List.of(below, above, middle)) { + final Address builder = expectedProposer(sorted, prev, 0); + for (int r = 1; r < N; r++) { + final Address rp = expectedProposer(sorted, prev, r); + final int idx = ids.indexOf(rp); + assertThat(rule.refusal(reproposed(h, r, builder, idx, idx), parentWithCoinbase(h, prev))) + .as("prev %s round %s", prev, r).isEmpty(); + judged++; + } + } + assertThat(judged).isEqualTo(3 * (N - 1)); + } + + // ---- AERE D-415 (2026-09-25): the vanity field is exactly 32 bytes at EVERY height, in the codec, as in the second client ---- + + /** The genesis extraData of chains 2800 and 28001 (both a 32-zero vanity), as published. */ + private static final String GENESIS_2800_EXTRA = + "0xf865a00000000000000000000000000000000000000000000000000000000000000000f83f94b5e768cb3117ea25175f51aa956b48691025c2a894f73bbbd26a82dbc769066550d8d4585eab2fc00d944bf6f91d84b37454544279cba152581ed6820044c080c0"; + private static final String GENESIS_28001_EXTRA = + "0xf87aa00000000000000000000000000000000000000000000000000000000000000000f8549404211fd71c1eae94f560e7e173399c91c15fd9b394070d5e1534a367430307a6e56bef29ddff79c0e394b84ec13c5811264f60a8bc392634709558223ea694fb6be1504acf8262cf98669f7833b9cf1cbfabb9c080c0"; + /** A real 32-byte vanity between anchors on 28001 today: the client version string, not a digest. */ + private static final String VANITY_VERSIUNE = "0x626573752032362e342e302d616572652e312d643230333230312d7774396239"; + + private BlockHeader cuVanity(final long h, final Bytes vanity) { + final BftExtraData ed = + new BftExtraData(vanity, List.of(), Optional.empty(), 0, ids, Collections.emptyList(), Collections.emptyList(), Optional.empty()); + return new BlockHeaderTestFixture().number(h).coinbase(ids.get(0)).extraData(CODEC.encode(ed)).buildHeader(); + } + + @Test + public void theVanityFieldIsExactly32BytesAtEveryHeightAndBothGenesesStillDecode() { + // the two real geneses and a real between-anchors vanity decode as before (the check cannot touch the chain) + for (final String g : List.of(GENESIS_2800_EXTRA, GENESIS_28001_EXTRA)) { + assertThat(CODEC.decodeRaw(Bytes.fromHexString(g)).getVanityData().size()).as("genesis").isEqualTo(32); + } + assertThat(CODEC.decode(cuVanity(H_HDR - 5, Bytes.fromHexString(VANITY_VERSIUNE))).getVanityData().size()).isEqualTo(32); + // found by the differential fuzzer: a 0-, 31-, 33- or 64-byte vanity between anchors was admitted here and refused by the + // second client; now the codec refuses it at any height, above and below H_hdr, and the header rule reports the reason + for (final int n : new int[] {0, 31, 33, 64}) { + final Bytes v = Bytes.wrap(new byte[n]); + assertThatThrownBy(() -> CODEC.decode(cuVanity(H_HDR + 12, v))).as("vanity %s above H_hdr", n) + .isInstanceOf(RLPException.class).hasMessageContaining("AERE-D415"); + assertThatThrownBy(() -> CODEC.decode(cuVanity(H_HDR - 5, v))).as("vanity %s below H_hdr", n) + .isInstanceOf(RLPException.class).hasMessageContaining("AERE-D415"); + assertThat(rule.refusal(cuVanity(H_HDR + 12, v))).get().asString().contains("does not decode").contains("AERE-D415"); + } + } + @Test public void belowHhdrTheUpstreamRuleDecidesAndHhdrNeedsHfin() { final BlockHeader below = sealedHeader(H_HDR - 1, 0, 0, 0, 0); diff --git a/anchor/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/PropagatedBlockAdoptionGate.java b/anchor/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/PropagatedBlockAdoptionGate.java new file mode 100644 index 0000000..7e11ed3 --- /dev/null +++ b/anchor/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/PropagatedBlockAdoptionGate.java @@ -0,0 +1,103 @@ +/* + * 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.ethereum.eth.sync; + +import org.hyperledger.besu.ethereum.core.BlockHeader; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AERE D-412 (2026-09-26), Rule C. A process-wide gate the block PROPAGATION path asks before it adopts + * a block arriving from the network as the new canonical head. + * + *

WHY IT EXISTS. From AIP-22 stage 5 (H_hdr) a header carries only the proposer's single Falcon-512 + * seal, so a valid header no longer proves a quorum agreed the block: an honest committed block and a + * block a single key forged are indistinguishable to header validation. The quorum agreement lives OFF + * the header, in the commit seals a validator heard (AIP-21, {@code PqSealCache}). Measured on rep28098, + * a single validator forged its own between-anchor block, gossiped it, and honest validators imported it + * as head and built on top. This gate lets a validator 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. Block VALIDITY is + * unchanged (this is fork choice, not validation), so the sync downloader - a different code path that + * does not consult this gate - still imports the same chain and catch-up is unaffected. + * + *

WHY A STATIC REGISTRY. This module ({@code ethereum/eth}) sits below the consensus modules and + * cannot depend on them, so the smart part of the decision (the seal cache, the registry, the validator + * set) lives in a consensus-aware implementation that registers itself here at startup, exactly as + * {@code PqSealCache.instance()} and the other AERE consensus singletons are process-wide. The interface + * takes only {@code ethereum/core} types, so there is no backward dependency. Unset, the gate is + * {@link #ALLOW_ALL} and behaviour is byte-for-byte as before. + * + *

FAIL OPEN. {@link #mayAdopt} never throws: a bug in the installed gate reverts to the current + * behaviour (adopt), never to refusing every block, because a repair that could stop the chain is worse + * than the exposure it closes. + */ +public final class PropagatedBlockAdoptionGate { + + private static final Logger LOG = LoggerFactory.getLogger(PropagatedBlockAdoptionGate.class); + + /** The decision a consensus-aware implementation supplies. */ + @FunctionalInterface + public interface Gate { + /** + * Whether this node may adopt a block that arrived by propagation as the new canonical head. + * + * @param header the propagated block's header, already header-validated + * @param parent the parent header, already on this node's chain + * @return true to adopt (import as head), false to leave this height to this node's own consensus + */ + boolean mayAdopt(BlockHeader header, BlockHeader parent); + } + + /** The default: adopt everything, i.e. exactly the behaviour before this gate existed. */ + public static final Gate ALLOW_ALL = (header, parent) -> true; + + private static volatile Gate gate = ALLOW_ALL; + + private PropagatedBlockAdoptionGate() {} + + /** + * Install the gate for this process. A consensus builder calls this at startup. + * + * @param g the gate, or null to reset to {@link #ALLOW_ALL} + */ + public static void install(final Gate g) { + gate = (g == null) ? ALLOW_ALL : g; + } + + /** Reset to {@link #ALLOW_ALL}. For tests and a deliberate operator reset. */ + public static void reset() { + gate = ALLOW_ALL; + } + + /** + * Whether the installed gate allows adopting this propagated block. Never throws: on any error it + * allows (fail open), so a gate bug can never stop the chain. + * + * @param header the propagated block's header + * @param parent the parent header + * @return true to adopt + */ + public static boolean mayAdopt(final BlockHeader header, final BlockHeader parent) { + try { + return gate.mayAdopt(header, parent); + } catch (final RuntimeException e) { + LOG.warn( + "AERE D-412: propagation adoption gate threw ({}); allowing the block, as before the gate.", + e.toString()); + return true; + } + } +}