anchor corpus: the consensus sources the fleet runs today (commit seal signed once per round, round-change round cap, anchor lapse guard, tests)

This commit is contained in:
Aere Network 2026-09-29 16:20:57 +03:00
parent 1779ac8a8b
commit 6765c0d2b2
13 changed files with 516 additions and 95 deletions

View File

@ -123,7 +123,7 @@ the section 4(b) notice of changed files), and each keeps its original notice pe
<!-- GENERAT: lista de mai jos se rescrie de unealta de licentiere din depozitul de constructie -->
## 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** |

View File

@ -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

View File

@ -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<FalconSeal> certificate, final long blockNumber) {
final long chainId, final Collection<FalconSeal> certificate, final long blockNumber) {
if (hasStrictlyIncreasingIndices(certificate)) {
return true;
}
return PqAnchorLapse.isDisarmed(blockNumber) && hasDistinctNonNegativeIndices(certificate);
return PqAnchorLapse.isDisarmed(chainId, blockNumber) && hasDistinctNonNegativeIndices(certificate);
}
/**

View File

@ -320,12 +320,20 @@ public final class PqAnchorLapse {
* by one rule and refused by the other is a chain break.
*
* <p>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.
*
* <p>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<Window> windowCovering(final long blockNumber) {
public static Optional<Window> windowCovering(final long chainId, final long blockNumber) {
if (chainId != CHAIN_ID) {
return Optional.empty();
}
return WINDOWS.stream().filter(w -> w.covers(blockNumber)).findFirst();
}

View File

@ -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);
}

View File

@ -179,17 +179,17 @@ public class PqAnchorTest {
final List<FalconSeal> 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<FalconSeal> 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<FalconSeal> 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

View File

@ -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<FalconSeal> pqSealedFalcon = Optional.empty();
private java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> 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> 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> falconSeal = pqSealedFalcon;
final java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> 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> 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> falconSeal = pqSealedFalcon;
final java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> 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<org.hyperledger.besu.consensus.common.bft.SchemeSeal> extraSealsFor(
java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> 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.
*
* <p>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<FalconSeal> 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<FalconSeal> falconSealFor(final QbftBlock block, final Hash commitHash) {
Optional<FalconSeal> 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.
//

View File

@ -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<Address> 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;
}

View File

@ -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.
*
* <p>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.
*
* <p>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<QbftMinedBlockObserver> 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)));
}
}

View File

@ -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();
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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<Integer> indices) {
final Bytes32 message = PqAnchor.commitMessage(chain, parent.getNumber(), parent.getHash().getBytes());
final List<FalconSeal> seals = new ArrayList<>();
for (final int index : indices) {
seals.add(new FalconSeal(index, LocalRegistry.sign(index, message)));
}
final List<FalconSeal> 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<FalconSeal> sealsFor(final BlockHeader parent, final List<Integer> 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