diff --git a/LICENSING.md b/LICENSING.md index f86368b..7e57deb 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -123,7 +123,7 @@ the section 4(b) notice of changed files), and each keeps its original notice pe -## anchor/: every file that carries someone else's copyright (generated 2026-09-27) +## anchor/: every file that carries someone else's copyright (generated 2026-09-29) This list is **generated from the files this package actually ships**, not written by hand: the previous hand-written table fell behind the tree on 2026-09-12 and seven upstream files went @@ -132,8 +132,8 @@ repository, and the publisher refuses to ship a package whose list disagrees wit You do not need that tool to check the list: every row below names a file in this package, and `grep -i copyright` on it shows the same line. -Measured 2026-09-27 over `anchor/`: **209 files**. **90** carry someone -else's copyright alone, **64** carry a shared notice naming both upstream and us, and **11** name only +Measured 2026-09-29 over `anchor/`: **211 files**. **91** carry someone +else's copyright alone, **64** carry a shared notice naming both upstream and us, and **12** name only `contributors to the AERE Network`; 44 carry no copyright line at all. The first two groups are the table below. A shared notice is listed too: those are precisely the upstream files we changed, so section 4(b) has something to say about each of them. @@ -252,6 +252,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayloadPqTest.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqHybridExtrasNeverThrowTest.java` | `Copyright contributors to Hyperledger Besu.` | upstream | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqLateSealSalvageTest.java` | `Copyright contributors to Besu.` | upstream | +| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereD434RoundCapTest.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityMessageTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityVectorsWriter.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuardTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt index fafdc9a..d69b3c7 100644 --- a/anchor/MANIFEST-sha256.txt +++ b/anchor/MANIFEST-sha256.txt @@ -25,9 +25,9 @@ dc4c68ad18d64aedae2363fc83f7a5329d258e749b9f7cbe145ba8496139785e consensus/comm 570f7825afdb0dd10e1d920f7de8ae21a85f196e77edbd8da8de94994d845839 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducer.java dbb4381155c4222491676da2e6cd13a8b15ce38c59a14ee8cb65edd3af1bf282 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupport.java 681d5ca2ea929d81f92664341d2651160d50621efb5194808627fb61834c292b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistry.java -ecd0e865aa48e461618048c13c46d9b54095dfa2e14180750f49b90faf49302a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java +fd878298fa54637e8ca5f80d5012dc1350dfe75ef4db381bdb4610cb093fcc06 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java 1c3d90324c6f5116194bae0539680fdc1f528ba4c893c5e2b44a592aea6b3abd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java -c6285ca43331781e1d350a07d58256d02b8b3a74b96b6469c0dca33ecfb6c8cd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java +c147c582ebdf1880f64142097487ade72b23228c9d58c313ab369f6cc7e078f6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java f27952a25bad02d2939a356f1bac8800ff8fb08023c6bdfb1c6d0e51b5a206da consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java 81ddbf2b40f79bf0e382c507235f07ad74937ffd455e387c265880bdcf4e2b5c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSyncModeGuard.java e16b2c654fe21893c7972dac71f3ffafd1e4e8efbecdb6f95051820e36fbd48e consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuard.java @@ -85,12 +85,12 @@ e96c6e79287a17eafef8f4ff5c32e539e489115b02f7afaceb8cdeff73562d69 consensus/comm 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 -e2487ff508ffb51bb61ca19531f4c4e68c5981611fd9fec5c92a2965f0bb8f47 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java +8fc425b416e6c48e1cdc3bfa2b4decebf205f58258832a8441e73b34c1e57a53 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java dabb059478da5259c309e6637e987ab35ef7b69cc286e41f936e4f3e83d3f082 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java fe84c239e3504f55e8d950e53b844641cc1dd7a7c864dd8f58db3d2f50c12b9d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCacheHygieneTest.java 0bf1c8a9cdd91d7c34053d6e5b4ff929787f34eb7aa7d6eed26fc5c94fa1c42b consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCostTest.java 23daeb4888c8de8337518a27ef60e42573ebafaf340075e8ce9236f0d8d580f6 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java -09c41c77408acc4711e4c46175485bf41babff0d0a8ec7481670fddfd28967b0 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java +a2f1c79b5516a255615c6b5172fc44669bc8f313a66e718c7120f46045bb819c consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java 6ff9586cf0d544590585ad27227f7775f1dd86c61ccdd475321180d4e53f47d2 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuardTest.java 39656762e621e76b33ced524314e9692cfa875d7e8db8dbb22aaa6b7f9ad5c14 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2ProducerTest.java 72e58681267664a864dba5a371ffbc6f296b524c2dee248893b17c12da113f00 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2Test.java @@ -128,7 +128,7 @@ a1405402e891189909a436abd9f83c863349fdcf01d3011542d805e2bb3911ed consensus/qbft e19b12e6db33cfb9bb1c2f22e321815130dfbcc5730bb505320f08a45a371093 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java 51eddf3cb8e37489abd35e6f4c36294a13ec76337bbbbfd54c07584085dceaac consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java 0a450699c702104dcef6bf8912b4d180f05d647288c75a9f78da6af3bd3d90f1 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java -6a34f5e87b5c5c629f1f08d0dc88c336a3ab3c0ba8797d3b0ee477246f10f4c6 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java +80613a8ac11ab48dfcecce5690a28da7d66dd8332584fd221f2875cb33d8c9d0 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java 4494f33ca028b60eaa093e6c24dd13706461e228f7f5a29e40e8917d1ef3b373 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java eadc4a14e2f0fdf7c1d9667d67c59f709af7a86fccf51923de0248b73e223c21 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java 4c0811d182f5252bdde31560f514a518db58c8d0c82c748a6c0f5d68472470c6 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java @@ -140,16 +140,18 @@ bacf232d9f3a9163ce6df3091c5e9161efcb26a73aecaead910e6952ee27bf86 consensus/qbft afb487f8c1dd87e323fdc1153ecfdf8ec6cb5a10e0726783d4214c07a3fa9a95 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqRoundChangeEnforcement.java 3aaa572c81641ad9a3e9cef9460b9a82dc4faff8080b933ef0e835881cf30bb4 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PrepareValidator.java 2f0001ca7b302b38b81e51fee14120c385070290afc2754a75ec581d170b4943 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/ProposalPayloadValidator.java -a82c7665c8c7dc1115f5523275608bf278a71fb6c6fb0e49895fa5321cdc9cbf consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidator.java +afa427cea95acd4d1f54f6824d39fae4125a7d4e23a84785f10b84ae9fe756ad consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidator.java e2107737579c16b73cbd2c9b50e90abcd6c910309286fb862cef780555ca43a5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/network/ProposalSealPlumbingTest.java 6da7d76a2ddc3dcab2a6d54d60615bb54182594f230e858cfd0c13b2626b76e0 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/network/RoundChangeSealPlumbingTest.java 36dd77cdea980e4de0090e86ae2ca75d1af62a9477fe056c7fdde06422617898 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayloadHybridTest.java fe25255de4b5fa6d86412fa4c48218180f549f0a4ffea5b5cc0a7d19a5be4cd9 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayloadPqTest.java a994d5638940ea90f37ed6f3ed5eebc24781707f2f4555c7a079d1251ddbf893 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayloadPqTest.java df5d7d2119cf0864a8c2dce50f4f69bd8b64804cfda7f7366c013b304af0a6e7 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayloadPqTest.java +0c78dcd73a795f9d2736910ee695af35616c7bf57f3463fdd9eccb82f7e12322 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqCommitSealsSignedOnceTest.java 97ac1e46ab37238a2b6232eccdd4636ac6fa928c8e3d4f8a473c083c5d5c3b39 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqHybridExtrasNeverThrowTest.java 0bb2c1e31a7f29e9b07fb7030567746fc6d79460ed4739d5ecafac44501f7bb5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqLateSealSalvageTest.java 127de5fbe8014768a657f31bc77d7e66557273febdf3412a5536e8628fd70c5c consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqSealsWithoutProposalTest.java +b1a6fdc177ccca180afdb20a7946ee33437d47a12cd6f5aade6e02238213bcf9 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereD434RoundCapTest.java c1b31f5c357a1966cdd1e963369a1eb3078a381679c6adc8fb5206f754eca84b consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityMessageTest.java 70bc4d5242e5f5560333c862bef0b2d5167649aabb3cd66fe61a101678d30b4d consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityVectorsWriter.java 11c968ba3eb4adabb548af5a0f6192b21ec75ff01431cc678fdfc6c13f0d4937 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereIdentityMessageTest.java @@ -178,8 +180,8 @@ cfb0aad408ab620f28b09914ece7d8d3d09f9e6bbb503070e28d9ef39c023dee consensus/qbft cbec6f4e280250ddefe5c79c453b59a07be6435e6f88d5614e92168c855e4693 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRule.java e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRule.java ab0fcd8722dcb76560f0ef8fda8af9c2b8b6ec9b468326aaf616c853526f7f81 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestAttachedRule.java -63313f944fcda0db45c40d4df840a465e6147cc85997b8ca832f0cfefea97987 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java -0f348ac4fa519f27f52f631dd290eab6e84980739a09ec767c8ca69e4a4f8f64 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java +e143ae2b1cdf4af5d8c7f10155c9991079ebfaef1a8aa8b9678e0108f1a56d68 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java +fc3de7c3d3599c1ff0a651d7fa4f33774abf20ad646469009d339cc1e68b3325 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 e0f8afc69fef826c5ee8a95745a967b1049ce10b9d591696a4fc095fb28062cb consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java @@ -190,7 +192,7 @@ e0f8afc69fef826c5ee8a95745a967b1049ce10b9d591696a4fc095fb28062cb consensus/qbft ad018cba0a3fe7f018b11c6c6a2d45e3f5547342ec45b620a4df2595801ae71a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealLogThrottleTest.java 3895d10bcf5ffbdaf0506503a0d9e3d72c600288def268fd5e9f68c4a042e162 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRuleRetirementTest.java e2df575ee4d6ab5bd961b0886ece3d3c392a50193f1d3256a438a72da9e20d10 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRuleTest.java -77d16fd35ca5607e9702f39cfd9a24a7e8523f631a73e82138763e6d5b718e7b consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java +e52370559c2237d39e9365335cb562ae165e0dc904d18f32ef57fa264390d014 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java 535cca4f1083c8a25bd29877e988b99704666853c1fe8b0ba32d71efe4e15f37 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java 2a76e2500e7d26fc2443f4a37211d59eb1e80e736430dca0785dd2f3f6ea1daf consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorV2RulesTest.java diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java index 32c634e..5524158 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java @@ -520,16 +520,17 @@ public final class PqAnchor { * sortedness, so no new unsorted header can ever be written, at any height. The window is closed by * construction: it is bounded above by a measured constant, so it cannot grow. * + * @param chainId the chain of the header; the unordered exception exists on chain 2800 only (D-428) * @param certificate the carried certificate * @param blockNumber the number of the header carrying it * @return true iff the ordering is acceptable for a header at that height */ public static boolean hasAcceptableIndices( - final Collection certificate, final long blockNumber) { + final long chainId, final Collection certificate, final long blockNumber) { if (hasStrictlyIncreasingIndices(certificate)) { return true; } - return PqAnchorLapse.isDisarmed(blockNumber) && hasDistinctNonNegativeIndices(certificate); + return PqAnchorLapse.isDisarmed(chainId, blockNumber) && hasDistinctNonNegativeIndices(certificate); } /** diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java index a7c8c9b..0c4cc20 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java @@ -320,12 +320,20 @@ public final class PqAnchorLapse { * by one rule and refused by the other is a chain break. * *

It is deliberately FALSE inside a threshold-only range. There the digest binding and the - * ordering did hold and are still required; only {@link #historicMinSeals(long)} moves. + * ordering did hold and are still required; only {@link #historicMinSeals(long, long)} moves. * + *

AERE D-428 (2026-09-28): the ranges are facts about chain {@value #CHAIN_ID} and about no other chain. Before this the + * predicates did not take the chain, so a node of ANY other network built from this source turned the anchor rules off over + * the same heights, while the second client (PqAnchorLapse.cs) never did: two clients judging one header differently. + * + * @param chainId the chain of the header being judged * @param blockNumber the height being judged * @return true iff the height falls inside a range where nothing was in force */ - public static boolean isDisarmed(final long blockNumber) { + public static boolean isDisarmed(final long chainId, final long blockNumber) { + if (chainId != CHAIN_ID) { + return false; + } for (final Window window : WINDOWS) { if (window.covers(blockNumber) && window.relaxation() == Relaxation.EVERYTHING) { return true; @@ -342,10 +350,14 @@ public final class PqAnchorLapse { * from a fixed range instead of from a runtime option. A node validating history therefore asks * the certificate for what was asked of the proposer that wrote it, and nothing more. * + * @param chainId the chain of the header being judged; any chain but {@value #CHAIN_ID} gets empty (D-428) * @param blockNumber the height being judged * @return the threshold in force, or empty when the configured schedule applies unchanged */ - public static OptionalInt historicMinSeals(final long blockNumber) { + public static OptionalInt historicMinSeals(final long chainId, final long blockNumber) { + if (chainId != CHAIN_ID) { + return OptionalInt.empty(); + } for (final Window window : WINDOWS) { if (window.covers(blockNumber) && window.relaxation() == Relaxation.THRESHOLD_ONLY) { return OptionalInt.of(window.effectiveMinSeals()); @@ -357,10 +369,14 @@ public final class PqAnchorLapse { /** * The range covering a height, when there is one. * + * @param chainId the chain of the header being judged; any chain but {@value #CHAIN_ID} gets empty (D-428) * @param blockNumber the height being judged * @return the range, or empty */ - public static Optional windowCovering(final long blockNumber) { + public static Optional windowCovering(final long chainId, final long blockNumber) { + if (chainId != CHAIN_ID) { + return Optional.empty(); + } return WINDOWS.stream().filter(w -> w.covers(blockNumber)).findFirst(); } diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java index 038d862..2bd1894 100644 --- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java +++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java @@ -93,20 +93,20 @@ public class PqAnchorLapseTest { @Test public void isDisarmedIsTrueOnlyWhereNothingWasInForce() { - assertThat(PqAnchorLapse.isDisarmed(DISARMED_FIRST)).isTrue(); - assertThat(PqAnchorLapse.isDisarmed(DISARMED_LAST)).isTrue(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST)).isTrue(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, DISARMED_LAST)).isTrue(); // The neighbouring anchor heights, both measured to carry a digest, stay under the strict rules. - assertThat(PqAnchorLapse.isDisarmed(LAST_ENFORCED_BEFORE)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(DISARMED_FIRST - 1L)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(DISARMED_LAST + 1L)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, LAST_ENFORCED_BEFORE)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST - 1L)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, DISARMED_LAST + 1L)).isFalse(); // And FALSE across the whole second range: there the digest and the ordering did hold, and a // relaxation that leaked into it would throw away the binding on 19,425 anchor heights. - assertThat(PqAnchorLapse.isDisarmed(LOWERED_FIRST)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(13_500_016L)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(LOWERED_LAST)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(FIRST_FULLY_ENFORCED_AFTER)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 13_500_016L)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, LOWERED_LAST)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, FIRST_FULLY_ENFORCED_AFTER)).isFalse(); } @Test @@ -115,7 +115,7 @@ public class PqAnchorLapseTest { for (long height = DISARMED_FIRST; height <= DISARMED_LAST; height += PqAnchorLapse.ANCHOR_SPACING) { - assertThat(PqAnchorLapse.isDisarmed(height)) + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, height)) .describedAs("anchor height %d of the disarmed range", height) .isTrue(); covered++; @@ -123,11 +123,11 @@ public class PqAnchorLapseTest { assertThat(covered).isEqualTo(36L); // The rest of the chain is untouched. - assertThat(PqAnchorLapse.isDisarmed(0L)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(13_014_000L)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(13_000_000L)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(14_077_000L)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(Long.MAX_VALUE)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 0L)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 13_014_000L)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 13_000_000L)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 14_077_000L)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, Long.MAX_VALUE)).isFalse(); } // ----------------------------------------------------------------------------------------------- @@ -136,23 +136,23 @@ public class PqAnchorLapseTest { @Test public void historicMinSealsAnswersOnlyInsideTheLoweredRange() { - assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST)).hasValue(1); - assertThat(PqAnchorLapse.historicMinSeals(13_500_016L)).hasValue(1); - assertThat(PqAnchorLapse.historicMinSeals(LOWERED_LAST)).hasValue(1); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST)).hasValue(1); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, 13_500_016L)).hasValue(1); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOWERED_LAST)).hasValue(1); // One anchor step past the end and the configured schedule applies again, unchanged. This is // the assertion that stops the lowered threshold from becoming permanent. - assertThat(PqAnchorLapse.historicMinSeals(FIRST_FULLY_ENFORCED_AFTER)).isEmpty(); - assertThat(PqAnchorLapse.historicMinSeals(LOWERED_LAST + 1L)).isEmpty(); - assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST - 1L)).isEmpty(); - assertThat(PqAnchorLapse.historicMinSeals(LAST_ENFORCED_BEFORE)).isEmpty(); - assertThat(PqAnchorLapse.historicMinSeals(14_077_000L)).isEmpty(); - assertThat(PqAnchorLapse.historicMinSeals(Long.MAX_VALUE)).isEmpty(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, FIRST_FULLY_ENFORCED_AFTER)).isEmpty(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOWERED_LAST + 1L)).isEmpty(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST - 1L)).isEmpty(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LAST_ENFORCED_BEFORE)).isEmpty(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, 14_077_000L)).isEmpty(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, Long.MAX_VALUE)).isEmpty(); // And it does NOT answer inside the disarmed range: there the rules are skipped whole, so a // threshold would be a second, contradictory way of saying the same thing. - assertThat(PqAnchorLapse.historicMinSeals(DISARMED_FIRST)).isEmpty(); - assertThat(PqAnchorLapse.historicMinSeals(DISARMED_LAST)).isEmpty(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST)).isEmpty(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, DISARMED_LAST)).isEmpty(); } @Test @@ -161,19 +161,19 @@ public class PqAnchorLapseTest { // schedule asks, the counts seen are 1 and 2, and NOT ONE carries zero. A floor of zero would // therefore accept an empty certificate at a height where a certificate was in fact required, // which is weaker than the history needs. - assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST)).hasValue(1); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST)).hasValue(1); assertThat(PqAnchorLapse.windows().get(1).effectiveMinSeals()).isPositive(); } @Test public void windowCoveringNamesTheRangeOrNothing() { - assertThat(PqAnchorLapse.windowCovering(DISARMED_FIRST)).isPresent(); - assertThat(PqAnchorLapse.windowCovering(DISARMED_FIRST).get().lastBlock()) + assertThat(PqAnchorLapse.windowCovering(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST)).isPresent(); + assertThat(PqAnchorLapse.windowCovering(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST).get().lastBlock()) .isEqualTo(DISARMED_LAST); - assertThat(PqAnchorLapse.windowCovering(LOWERED_FIRST)).isPresent(); - assertThat(PqAnchorLapse.windowCovering(LOWERED_FIRST).get().relaxation()) + assertThat(PqAnchorLapse.windowCovering(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST)).isPresent(); + assertThat(PqAnchorLapse.windowCovering(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST).get().relaxation()) .isEqualTo(PqAnchorLapse.Relaxation.THRESHOLD_ONLY); - assertThat(PqAnchorLapse.windowCovering(FIRST_FULLY_ENFORCED_AFTER)).isEmpty(); + assertThat(PqAnchorLapse.windowCovering(PqAnchorLapse.CHAIN_ID, FIRST_FULLY_ENFORCED_AFTER)).isEmpty(); assertThat(PqAnchorLapse.earliestAffectedBlock()).contains(DISARMED_FIRST); } diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java index 5bea4d3..aabecee 100644 --- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java +++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java @@ -179,17 +179,17 @@ public class PqAnchorTest { final List real2 = Arrays.asList(seal(5), seal(6), seal(1), seal(0)); // Inside the window: accepted, at both edges and in the middle. - assertThat(PqAnchor.hasAcceptableIndices(real1, PqAnchor.UNORDERED_WINDOW_FIRST)).isTrue(); - assertThat(PqAnchor.hasAcceptableIndices(real2, PqAnchor.UNORDERED_WINDOW_LAST)).isTrue(); - assertThat(PqAnchor.hasAcceptableIndices(real1, PqAnchor.UNORDERED_WINDOW_FIRST + 32L)).isTrue(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real1, PqAnchor.UNORDERED_WINDOW_FIRST)).isTrue(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real2, PqAnchor.UNORDERED_WINDOW_LAST)).isTrue(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real1, PqAnchor.UNORDERED_WINDOW_FIRST + 32L)).isTrue(); // NEGATIVE CONTROL, and it is the whole point: one block on either side of the window and the // very same certificate is rejected. If these two ever pass, the exception has stopped being an // exception and has become the rule. - assertThat(PqAnchor.hasAcceptableIndices(real1, PqAnchor.UNORDERED_WINDOW_FIRST - 1L)).isFalse(); - assertThat(PqAnchor.hasAcceptableIndices(real1, PqAnchor.UNORDERED_WINDOW_LAST + 1L)).isFalse(); - assertThat(PqAnchor.hasAcceptableIndices(real2, 1L)).isFalse(); - assertThat(PqAnchor.hasAcceptableIndices(real2, Long.MAX_VALUE)).isFalse(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real1, PqAnchor.UNORDERED_WINDOW_FIRST - 1L)).isFalse(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real1, PqAnchor.UNORDERED_WINDOW_LAST + 1L)).isFalse(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real2, 1L)).isFalse(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real2, Long.MAX_VALUE)).isFalse(); } @Test @@ -199,24 +199,24 @@ public class PqAnchorTest { // unrepresentable, which is the only part of the ordering rule that an attacker cares about. final List repetat = Arrays.asList(new FalconSeal(2, signatureFor(2, 0)), new FalconSeal(2, signatureFor(2, 1))); - assertThat(PqAnchor.hasAcceptableIndices(repetat, PqAnchor.UNORDERED_WINDOW_FIRST)).isFalse(); - assertThat(PqAnchor.hasAcceptableIndices(repetat, PqAnchor.UNORDERED_WINDOW_LAST)).isFalse(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, repetat, PqAnchor.UNORDERED_WINDOW_FIRST)).isFalse(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, repetat, PqAnchor.UNORDERED_WINDOW_LAST)).isFalse(); // Negative indices and null signatures stay rejected inside the window too. assertThat( - PqAnchor.hasAcceptableIndices( + PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, List.of(new FalconSeal(-1, Bytes.of(1))), PqAnchor.UNORDERED_WINDOW_FIRST)) .isFalse(); assertThat( - PqAnchor.hasAcceptableIndices( + PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, List.of(new FalconSeal(1, null)), PqAnchor.UNORDERED_WINDOW_FIRST)) .isFalse(); // And a sorted certificate is accepted everywhere, window or not: the exception only ever adds. final List sortat = Arrays.asList(seal(0), seal(3), seal(6)); - assertThat(PqAnchor.hasAcceptableIndices(sortat, 1L)).isTrue(); - assertThat(PqAnchor.hasAcceptableIndices(sortat, PqAnchor.UNORDERED_WINDOW_FIRST)).isTrue(); - assertThat(PqAnchor.hasAcceptableIndices(sortat, Long.MAX_VALUE)).isTrue(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, sortat, 1L)).isTrue(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, sortat, PqAnchor.UNORDERED_WINDOW_FIRST)).isTrue(); + assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, sortat, Long.MAX_VALUE)).isTrue(); } @Test diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java index 9bb4499..189760f 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java @@ -105,6 +105,26 @@ public class QbftRound { // on-chain pre-image, so this is the same hash the parent header will carry once imported. private Hash pqOnchainHash; + // AERE D-337 (2026-09-28): the post-quantum seals of THIS node's Commit in this round, signed ONCE. + // + // Two paths emit this node's Commit: accepting the proposal (the local copy, and the wire copy in + // the rare case the node is already prepared) and becoming prepared later (the wire copy, the + // usual case). Each used to sign on its own, so at the parent of an anchor every Besu validator + // made two SLH-DSA signatures back to back on the consensus thread, the first before its Prepare + // and the second between "prepared" and its Commit. Measured on 2800 (2026-09-28, import times in + // ms on rpc2, scripts/masuratori/ritm-pe-propunator.mjs): the anchor-parent slot 1,036 ms against + // 599 for the other slots. The message is fixed by (block, commit hash), both fixed for the round + // once the proposal is accepted, so the second signing could only produce another valid seal over + // the same bytes. It now reuses the first, and the local and wire Commits carry the same seals + // (Falcon is randomised: before this, they carried two different valid Falcon seals). + // + // What is NOT reused: an attempt that produced no Falcon seal, or no extras where extras apply. + // Those are retried exactly as before, so a node whose first attempt failed keeps its second chance. + private Hash pqSealedCommitHash; + private Optional pqSealedFalcon = Optional.empty(); + private java.util.List pqSealedExtras = + java.util.List.of(); + /** * Instantiates a new Qbft round. * @@ -429,10 +449,12 @@ public class QbftRound { LOG.warn("Failed to construct commit seal; {}", e.getMessage()); return true; } - final Optional falconSeal = falconSealFor(block, commitHash); - // AERE HYBRID: the other schemes' extras, over the SAME message; empty on any node today. + // AERE HYBRID: the Falcon seal and the other schemes' extras, over the SAME message, signed + // once per round (D-337, pqSealCommitOnce). + pqSealCommitOnce(block, commitHash); + final Optional falconSeal = pqSealedFalcon; final java.util.List extraSeals = - falconSeal.isPresent() ? extraSealsFor(block, commitHash) : java.util.List.of(); + pqSealedExtras; // There are times handling a proposed block is enough to enter prepared. if (wasPrepared != roundState.isPrepared()) { @@ -487,10 +509,12 @@ public class QbftRound { try { final Hash commitHash = commitHashFor(block); final SECPSignature commitSeal = nodeKey.sign(Bytes32.wrap(commitHash.getBytes())); - final Optional falconSeal = falconSealFor(block, commitHash); - // AERE HIBRID: aceleasi extrase si pe drumul tarziu, ca cele doua locuri sa nu divearga. + // AERE HIBRID: aceleasi sigilii si pe drumul tarziu, ca cele doua locuri sa nu divearga; D-337: + // chiar ACELEASI, semnate o data la acceptarea propunerii, nu o a doua semnare SLH-DSA aici. + pqSealCommitOnce(block, commitHash); + final Optional falconSeal = pqSealedFalcon; final java.util.List extraSeals = - falconSeal.isPresent() ? extraSealsFor(block, commitHash) : java.util.List.of(); + pqSealedExtras; if (!extraSeals.isEmpty()) { transmitter.multicastCommit( getRoundIdentifier(), block.getHash(), commitSeal, falconSeal, extraSeals); @@ -687,20 +711,49 @@ public class QbftRound { * hybrid-configured, and below the emission gate: {@link HybridSealProducer} never throws and * never emits half a certificate. */ - private java.util.List extraSealsFor( + java.util.List extraSealsFor( final QbftBlock block, final Hash commitHash) { // ANCHOR PARENTS ONLY (2026-09-03, D-329): an SLH-DSA-128s signature costs seconds here; it is // only ever used in the next anchor's certificate, which is built over the anchor's parent. So // the extras are signed only when the block being committed is the parent of an anchor height. // The same predicate gates the enforcement (PqCommitEnforcement.anchorParentByConfig). final long number = block.getHeader().getNumber(); - if (!org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.config() - .anchorAppliesAt(number + 1L)) { + if (!pqExtrasApplyAt(number)) { return java.util.List.of(); } return hybridExtrasOrEmpty(number, pqSealMessageFor(block, commitHash)); } + /** The anchor-parent predicate of {@link #extraSealsFor}, in one place for it and the D-337 reuse. */ + static boolean pqExtrasApplyAt(final long number) { + return org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.config() + .anchorAppliesAt(number + 1L); + } + + /** + * D-337 (2026-09-28): signs this node's Commit seals for this round at most once. A result is + * reused only when it is complete - a Falcon seal, and the extras wherever extras apply - so an + * attempt that failed is retried on the next path exactly as before this change. + * + *

Package-private, with {@link #falconSealFor} and {@link #extraSealsFor}, for the probe that + * counts the signings on the two paths (PqCommitSealsSignedOnceTest). + * + * @param block the proposed block of this round + * @param commitHash its commit hash in this round + */ + void pqSealCommitOnce(final QbftBlock block, final Hash commitHash) { + if (commitHash.equals(pqSealedCommitHash) + && pqSealedFalcon.isPresent() + && (!pqSealedExtras.isEmpty() || !pqExtrasApplyAt(block.getHeader().getNumber()))) { + return; + } + final Optional falcon = falconSealFor(block, commitHash); + pqSealedExtras = + falcon.isPresent() ? extraSealsFor(block, commitHash) : java.util.List.of(); + pqSealedFalcon = falcon; + pqSealedCommitHash = commitHash; + } + /** * D-334 (2026-09-03, mainnet 2800, 125 s of halted chain): the hybrid loader is lazy, so a * registry the container could not read surfaced here, as an exception thrown INTO the QBFT @@ -739,7 +792,7 @@ public class QbftRound { block.getHeader().getNumber(), () -> pqOnchainHashOf(block).getBytes(), commitHash); } - private Optional falconSealFor(final QbftBlock block, final Hash commitHash) { + Optional falconSealFor(final QbftBlock block, final Hash commitHash) { // FalconSealSupport.sign never throws (any fault is swallowed and logged), and returns empty // when this node holds no Falcon signing key, so the ECDSA commit path is never affected. // diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidator.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidator.java index 8fd9bed..63a94d5 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidator.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidator.java @@ -55,6 +55,9 @@ public class RoundChangePayloadValidator { private static final String ERROR_PREFIX = "Invalid RoundChange Payload"; private static final Logger LOG = LoggerFactory.getLogger(RoundChangePayloadValidator.class); + /** maximum allowed round (AERE D-434: ported from upstream 118f8b1920, GHSA-23rh-rrqg-wq82) */ + public static final int MAX_ALLOWED_ROUND = 1000; + private final Collection

validators; private final long chainHeight; private final PqRoundChangeEnforcement pqEnforcement; @@ -109,8 +112,8 @@ public class RoundChangePayloadValidator { } final int targetRound = payload.getRoundIdentifier().getRoundNumber(); - if (targetRound <= 0) { - LOG.info("{}: must contain a positive target round number", ERROR_PREFIX); + if (targetRound <= 0 || targetRound > MAX_ALLOWED_ROUND) { + LOG.info("{}: target round number out of range [1, {}]", ERROR_PREFIX, MAX_ALLOWED_ROUND); return false; } diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqCommitSealsSignedOnceTest.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqCommitSealsSignedOnceTest.java new file mode 100644 index 0000000..daa5ff1 --- /dev/null +++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqCommitSealsSignedOnceTest.java @@ -0,0 +1,210 @@ +/* + * Copyright contributors to 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.qbft.core.statemachine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; +import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.PqSealCache; +import org.hyperledger.besu.consensus.common.bft.RoundTimer; +import org.hyperledger.besu.consensus.common.bft.SchemeSeal; +import org.hyperledger.besu.consensus.qbft.core.QbftBlockTestFixture; +import org.hyperledger.besu.consensus.qbft.core.network.QbftMessageTransmitter; +import org.hyperledger.besu.consensus.qbft.core.payload.MessageFactory; +import org.hyperledger.besu.consensus.qbft.core.types.QbftBlock; +import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec; +import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCreator; +import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockHeader; +import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockInterface; +import org.hyperledger.besu.consensus.qbft.core.types.QbftMinedBlockObserver; +import org.hyperledger.besu.consensus.qbft.core.types.QbftProtocolSchedule; +import org.hyperledger.besu.consensus.qbft.core.validation.MessageValidator; +import org.hyperledger.besu.consensus.qbft.core.validation.QbftBlockHeaderTestFixture; +import org.hyperledger.besu.cryptoservices.NodeKey; +import org.hyperledger.besu.cryptoservices.NodeKeyUtils; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.util.Subscribers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * D-337 (2026-09-28): this node's Commit seals are signed ONCE per round, not once per path. + * + *

The measured cost on chain 2800: at the parent of every anchor each Besu validator signed + * SLH-DSA twice back to back on the consensus thread - on accepting the proposal (before its + * Prepare) and again on becoming prepared (between "prepared" and its Commit) - and the + * anchor-parent slot ran at 1,036 ms against 599 for the rest. + * + *

The round is driven through the real state machine: a peer's proposal, the local Prepare, a + * peer's Prepare that makes the round prepared, and the Commit that goes on the wire. The signers + * are stubbed on a spy so the SIGNINGS can be counted; everything between them is the production + * code. The pair of the claim is the retry: an attempt that produced no Falcon seal must be signed + * again on the second path, exactly as before the change, so the reuse cannot swallow a failure. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class PqCommitSealsSignedOnceTest { + + private static final long HEIGHT = 7L; + + private final NodeKey nodeKey = NodeKeyUtils.generate(); + private final Address localAddress = Address.extract(nodeKey.getPublicKey()); + private final NodeKey peerKey = NodeKeyUtils.generate(); + private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(HEIGHT, 0); + private final Subscribers subscribers = Subscribers.create(); + private final FalconSeal falcon = new FalconSeal(4, Bytes.fromHexString("0x29aabbcc")); + private final SchemeSeal slh = new SchemeSeal((byte) 0x02, 4, Bytes.fromHexString("0x0102030405")); + + private MessageFactory localFactory; + private MessageFactory peerFactory; + private QbftBlock proposedBlock; + + @Mock private QbftBlockCodec blockEncoder; + @Mock private QbftProtocolSchedule protocolSchedule; + @Mock private QbftMessageTransmitter transmitter; + @Mock private MessageValidator messageValidator; + @Mock private RoundTimer roundTimer; + @Mock private QbftBlockCreator blockCreator; + @Mock private QbftBlockInterface blockInterface; + + private final QbftBlockHeader parentHeader = + new QbftBlockHeaderTestFixture().number(HEIGHT - 1).buildHeader(); + + @BeforeEach + public void setup() { + PqSealCache.instance().clear(); + localFactory = new MessageFactory(nodeKey, blockEncoder); + peerFactory = new MessageFactory(peerKey, blockEncoder); + when(messageValidator.validateProposal(any())).thenReturn(true); + when(messageValidator.validatePrepare(any())).thenReturn(true); + when(messageValidator.validateCommit(any())).thenReturn(true); + proposedBlock = + new QbftBlockTestFixture() + .blockHeader(new QbftBlockHeaderTestFixture().number(HEIGHT).buildHeader()) + .build(); + when(blockInterface.replaceRoundForCommitBlock(any(), anyInt())).thenReturn(proposedBlock); + } + + @AfterEach + public void clean() { + PqSealCache.instance().clear(); + } + + /** A round of quorum 2: the local Prepare plus ONE peer Prepare make it prepared. */ + private QbftRound spiedRound() { + final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator); + return spy( + new QbftRound( + roundState, + blockCreator, + blockInterface, + protocolSchedule, + subscribers, + nodeKey, + localAddress, + localFactory, + transmitter, + roundTimer, + parentHeader)); + } + + /** Proposal from the peer, then the peer's Prepare: the second path fires on the second step. */ + private void driveToPrepared(final QbftRound round) { + round.handleProposalMessage( + peerFactory.createProposal( + roundIdentifier, proposedBlock, Collections.emptyList(), Collections.emptyList())); + assertThat(round.getRoundState().isPrepared()).isFalse(); + round.handlePrepareMessage(peerFactory.createPrepare(roundIdentifier, proposedBlock.getHash())); + assertThat(round.getRoundState().isPrepared()).isTrue(); + } + + @Test + public void anAnchorParentCommitIsSignedOnceAndTheWireCarriesThoseSeals() { + final QbftRound round = spiedRound(); + doReturn(Optional.of(falcon)).when(round).falconSealFor(any(), any()); + doReturn(List.of(slh)).when(round).extraSealsFor(any(), any()); + + driveToPrepared(round); + + verify(round, times(1)).falconSealFor(any(), any()); + verify(round, times(1)).extraSealsFor(any(), any()); + verify(transmitter, times(1)) + .multicastCommit( + eq(roundIdentifier), + eq(proposedBlock.getHash()), + any(), + eq(Optional.of(falcon)), + eq(List.of(slh))); + // the local Commit, added on accepting the proposal, carries the very same extras + assertThat(round.getRoundState().getExtraSeals()).containsExactly(slh); + } + + @Test + public void aFalconOnlyCommitOffTheExtrasGridIsAlsoSignedOnce() { + // off the anchor-parent predicate the extras are legitimately empty; a Falcon-only result is + // complete there and must be reused too, not re-signed + assertThat(QbftRound.pqExtrasApplyAt(HEIGHT)).isFalse(); + final QbftRound round = spiedRound(); + doReturn(Optional.of(falcon)).when(round).falconSealFor(any(), any()); + doReturn(List.of()).when(round).extraSealsFor(any(), any()); + + driveToPrepared(round); + + verify(round, times(1)).falconSealFor(any(), any()); + verify(transmitter, times(1)) + .multicastCommit( + eq(roundIdentifier), eq(proposedBlock.getHash()), any(), eq(Optional.of(falcon))); + } + + @Test + public void aFirstAttemptWithoutAFalconSealIsSignedAgainOnTheSecondPath() { + // the pair: reuse must never swallow a failed attempt; before D-337 the second path signed + // anew and could succeed, and it still must + final QbftRound round = spiedRound(); + doReturn(Optional.empty()).doReturn(Optional.of(falcon)).when(round).falconSealFor(any(), any()); + doReturn(List.of(slh)).when(round).extraSealsFor(any(), any()); + + driveToPrepared(round); + + verify(round, times(2)).falconSealFor(any(), any()); + verify(transmitter, times(1)) + .multicastCommit( + eq(roundIdentifier), + eq(proposedBlock.getHash()), + any(), + eq(Optional.of(falcon)), + eq(List.of(slh))); + } +} diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereD434RoundCapTest.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereD434RoundCapTest.java new file mode 100644 index 0000000..e719908 --- /dev/null +++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereD434RoundCapTest.java @@ -0,0 +1,61 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Aere Network notice (Apache-2.0 section 4(b)): this file was added by Aere Network, 2026-09-28 (D-434), as the regression test of + * the ported upstream fix hyperledger/besu 118f8b1920 (GHSA-23rh-rrqg-wq82, "Cap BFT round change number"). + */ +package org.hyperledger.besu.consensus.qbft.core.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; +import org.hyperledger.besu.consensus.common.bft.payload.SignedData; +import org.hyperledger.besu.consensus.qbft.core.payload.RoundChangePayload; +import org.hyperledger.besu.cryptoservices.NodeKey; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; + +/** + * AERE D-434: a RoundChange for a round above RoundChangePayloadValidator.MAX_ALLOWED_ROUND (1000) is refused, as upstream does since + * 2026-08-20. Before, any positive round was accepted, so one validator could push the round-change bookkeeping of every node to an + * arbitrary round. The worst round the live chains have reached is 6 (2026-09-04), so the cap never binds an honest network. + */ +class AereD434RoundCapTest { + + private static final long HEIGHT = 5L; + + private static boolean judge(final int round) { + final QbftNodeList validators = QbftNodeList.createNodes(4, null); + final RoundChangePayloadValidator v = new RoundChangePayloadValidator(validators.getNodeAddresses(), HEIGHT); + final RoundChangePayload payload = new RoundChangePayload(new ConsensusRoundIdentifier(HEIGHT, round), Optional.empty()); + final NodeKey key = validators.getNode(0).getNodeKey(); + return v.validate(SignedData.create(payload, key.sign(Bytes32.wrap(payload.hashForSignature().getBytes())))); + } + + @Test + void aRoundAboveTheCapIsRefused() { + assertThat(judge(RoundChangePayloadValidator.MAX_ALLOWED_ROUND + 1)).withFailMessage("round 1001 accepted").isFalse(); + assertThat(judge(Integer.MAX_VALUE)).withFailMessage("round 2^31-1 accepted").isFalse(); + } + + @Test + void theCapItselfAndOrdinaryRoundsAreStillAccepted() { + // positive control: the fixture is valid, and the cap is inclusive + assertThat(judge(1)).isTrue(); + assertThat(judge(RoundChangePayloadValidator.MAX_ALLOWED_ROUND)).isTrue(); + } +} diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java index 3b80c43..a9ba7a2 100644 --- a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java +++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java @@ -136,7 +136,7 @@ public class PqAnchorDigestRule implements DetachedBlockHeaderValidationRule { // at a height the whole network holds. See PqAnchorLapse for what happened, how the bounds were // measured, and why an exception written anywhere other than validation moves the error instead // of ending it. - if (PqAnchorLapse.isDisarmed(number)) { + if (PqAnchorLapse.isDisarmed(config.chainId(), number)) { return acceptAsWrittenInsideLapse(header, number); } @@ -174,7 +174,7 @@ public class PqAnchorDigestRule implements DetachedBlockHeaderValidationRule { // unsorted during a production interruption and a node syncing from genesis would otherwise // stop at the first of them and never pass it. Distinctness is not relaxed at any height, so a // repeated index stays unrepresentable everywhere. See PqAnchor.hasAcceptableIndices. - if (!PqAnchor.hasAcceptableIndices(certificate, number)) { + if (!PqAnchor.hasAcceptableIndices(config.chainId(), certificate, number)) { // Checked HERE, not only in the seals rule, so reordering and duplicate indices are caught // in LIGHT validation too, and so that "the order given" and "sorted" are the same list for // every header this rule accepts. @@ -277,14 +277,14 @@ public class PqAnchorDigestRule implements DetachedBlockHeaderValidationRule { + "validator index or carries a negative one. That is not a shape any header of " + "that range has, and it is the one property the exception does not relax.", number, - PqAnchorLapse.windowCovering(number).orElse(null)); + PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null)); return false; } LOG.debug( "AERE PQ ANCHOR (R1): block {} accepted as written; it falls inside the historical range " + "{} in which the anchor rules were not in force, so it carries no digest to bind.", number, - PqAnchorLapse.windowCovering(number).orElse(null)); + PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null)); return true; } catch (final Exception e) { LOG.warn( diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java index 694c01d..3f58217 100644 --- a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java +++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java @@ -143,7 +143,7 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule { // by one and refused by the other would be a chain break. Inside a named window the threshold // was not in force when the header was written and the indices were written in arrival order, // so neither can be required of it now. - if (PqAnchorLapse.isDisarmed(number)) { + if (PqAnchorLapse.isDisarmed(config.chainId(), number)) { return acceptAsWrittenInsideLapse(header, number); } @@ -205,13 +205,13 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule { // reproduce; asking for the ceiling rather than for nothing keeps the strongest claim the // range supports, and the ceiling is a measured floor, never zero. See PqAnchorLapse. int required = config.minSealsAt(number); - final OptionalInt inForce = PqAnchorLapse.historicMinSeals(number); + final OptionalInt inForce = PqAnchorLapse.historicMinSeals(config.chainId(), number); if (inForce.isPresent() && inForce.getAsInt() < required) { LOG.debug( "AERE PQ ANCHOR (R2): block {} sits in the historical range {}, so the threshold " + "applied is the {} that was in force then, not the {} the schedule asks for.", number, - PqAnchorLapse.windowCovering(number).orElse(null), + PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null), inForce.getAsInt(), required); required = inForce.getAsInt(); @@ -230,7 +230,7 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule { // Height aware since 2026-08-11, same measured historical window as rule R1. See // PqAnchor.hasAcceptableIndices for why the exception sits at VALIDATION and nowhere else, and // for what it deliberately does NOT relax. - if (!PqAnchor.hasAcceptableIndices(certificate, number)) { + if (!PqAnchor.hasAcceptableIndices(config.chainId(), certificate, number)) { LOG.warn( "AERE PQ ANCHOR (R2): block {} REJECTED - the certificate's validator indices are not " + "strictly increasing.", @@ -536,7 +536,7 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule { + "which the anchor rules were not in force, but its certificate repeats a " + "validator index or carries a negative one, which no header of that range does.", number, - PqAnchorLapse.windowCovering(number).orElse(null)); + PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null)); return false; } LOG.debug( @@ -544,7 +544,7 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule { + "historical range {} in which neither the threshold nor the ordering was in force.", number, certificate.size(), - PqAnchorLapse.windowCovering(number).orElse(null)); + PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null)); return true; } catch (final Exception e) { LOG.warn( diff --git a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java index 9451ec7..e7604a4 100644 --- a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java +++ b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java @@ -139,12 +139,12 @@ public class PqAnchorDisarmedWindowTest { .describedAs("the schedule asks for three seals at height %d", height) .isEqualTo(3); } - assertThat(PqAnchorLapse.isDisarmed(OFF_FIRST)).isTrue(); - assertThat(PqAnchorLapse.isDisarmed(OFF_LAST)).isTrue(); - assertThat(PqAnchorLapse.isDisarmed(BEFORE)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(LOW_FIRST)).isFalse(); - assertThat(PqAnchorLapse.historicMinSeals(LOW_FIRST)).hasValue(1); - assertThat(PqAnchorLapse.historicMinSeals(AFTER)).isEmpty(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, OFF_FIRST)).isTrue(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, OFF_LAST)).isTrue(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, BEFORE)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, LOW_FIRST)).isFalse(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOW_FIRST)).hasValue(1); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, AFTER)).isEmpty(); } // ----------------------------------------------------------------------------------------------- @@ -383,9 +383,9 @@ public class PqAnchorDisarmedWindowTest { // exception having leaked one block down. assertThat(live.anchorAppliesAt(OFF_FIRST - 1L)).isFalse(); assertThat(live.anchorAppliesAt(OFF_LAST + 1L)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(OFF_FIRST - 1L)).isFalse(); - assertThat(PqAnchorLapse.isDisarmed(OFF_LAST + 1L)).isFalse(); - assertThat(PqAnchorLapse.historicMinSeals(LOW_LAST + 1L)).isEmpty(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, OFF_FIRST - 1L)).isFalse(); + assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, OFF_LAST + 1L)).isFalse(); + assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOW_LAST + 1L)).isEmpty(); } // ----------------------------------------------------------------------------------------------- @@ -428,6 +428,67 @@ public class PqAnchorDisarmedWindowTest { assertThat(r2.validate(block, parent, contextWith(VALIDATORS))).isTrue(); } + // ----------------------------------------------------------------------------------------------- + // D-428 (2026-09-28): the ranges are facts about chain 2800. The same heights on ANY other chain are + // judged by the plain rules, as the second client does. Each case carries its positive control, so a + // refusal here cannot come from a signature made for the wrong chain. + // ----------------------------------------------------------------------------------------------- + + private static final long OTHER_CHAIN = 28001L; + + private final PqAnchorConfig otherChain = + new PqAnchorConfig( + OTHER_CHAIN, + H_LIVE, + Map.of(H_LIVE, 0, 13_034_000L, 3), + OptionalInt.empty(), + false, + OptionalInt.empty(), + OptionalInt.of(INTERVAL)); + + private final PqAnchorDigestRule r1Other = new PqAnchorDigestRule(otherChain); + private final PqAnchorSealsRule r2Other = new PqAnchorSealsRule(otherChain, registry); + + @Test + public void onAnotherChainTheFirstRangeIsJudgedByThePlainRules() { + for (final long height : List.of(OFF_FIRST, OFF_MIDDLE, OFF_LAST)) { + final BlockHeader parent = parentHeader(height - 1L); + final ProtocolContext context = contextWith(VALIDATORS); + final BlockHeader unenforced = unenforcedHeader(height, parent, ARRIVAL_ORDER); + assertThat(r1.validate(unenforced, parent)) + .describedAs("positive control: on chain 2800 height %d is inside the first range", height) + .isTrue(); + assertThat(r1Other.validate(unenforced, parent)) + .describedAs("on chain %d there was no lapse: no digest must be refused at %d", OTHER_CHAIN, height) + .isFalse(); + assertThat(r2Other.validate(unenforced, parent, context)) + .describedAs("on chain %d the arrival order and the missing digest are refused at %d", OTHER_CHAIN, height) + .isFalse(); + final BlockHeader honest = honestBlockOn(OTHER_CHAIN, height, parent, List.of(0, 1, 2)); + assertThat(r1Other.validate(honest, parent)) + .describedAs("positive control: an honest header OF chain %d is accepted at %d", OTHER_CHAIN, height) + .isTrue(); + assertThat(r2Other.validate(honest, parent, context)).isTrue(); + } + } + + @Test + public void onAnotherChainTheSecondRangeKeepsTheScheduledThreshold() { + for (final long height : List.of(LOW_FIRST, LOW_MIDDLE, LOW_LAST)) { + final BlockHeader parent = parentHeader(height - 1L); + final ProtocolContext context = contextWith(VALIDATORS); + assertThat(r2.validate(honestBlock(height, parent, List.of(5)), parent, context)) + .describedAs("positive control: on chain 2800 one seal passes at %d, the threshold in force then", height) + .isTrue(); + assertThat(r2Other.validate(honestBlockOn(OTHER_CHAIN, height, parent, List.of(5)), parent, context)) + .describedAs("on chain %d the threshold at %d is the scheduled three, not the 2800 lapse", OTHER_CHAIN, height) + .isFalse(); + assertThat(r2Other.validate(honestBlockOn(OTHER_CHAIN, height, parent, List.of(0, 1, 5)), parent, context)) + .describedAs("positive control: three seals OF chain %d pass at %d", OTHER_CHAIN, height) + .isTrue(); + } + } + // ----------------------------------------------------------------------------------------------- // Helpers. // ----------------------------------------------------------------------------------------------- @@ -460,6 +521,19 @@ public class PqAnchorDisarmedWindowTest { return header(number, parent.getHash(), extraData(digest, sorted)); } + /** An honest header for ANY chain: the digest and the seals are computed for that chain (D-428). */ + private BlockHeader honestBlockOn( + final long chain, final long number, final BlockHeader parent, final List indices) { + final Bytes32 message = PqAnchor.commitMessage(chain, parent.getNumber(), parent.getHash().getBytes()); + final List seals = new ArrayList<>(); + for (final int index : indices) { + seals.add(new FalconSeal(index, LocalRegistry.sign(index, message))); + } + final List sorted = PqAnchor.sortedByIndex(seals); + final Bytes32 digest = PqAnchor.anchorDigest(chain, parent.getNumber(), parent.getHash().getBytes(), sorted); + return header(number, parent.getHash(), extraData(digest, sorted)); + } + private List sealsFor(final BlockHeader parent, final List indices) { // One return, one mutability. An early return of an immutable empty list here is what // errorprone's MixedMutabilityReturnType refuses, and it is refusing something real: the empty