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 --> <!-- 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 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 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 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. `grep -i copyright` on it shows the same line.
Measured 2026-09-27 over `anchor/`: **209 files**. **90** carry someone 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 **11** name only 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. `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 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. 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/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/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/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/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/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** | | `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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 e143ae2b1cdf4af5d8c7f10155c9991079ebfaef1a8aa8b9678e0108f1a56d68 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 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 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 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 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 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 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 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 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 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 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 * 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. * 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 certificate the carried certificate
* @param blockNumber the number of the header carrying it * @param blockNumber the number of the header carrying it
* @return true iff the ordering is acceptable for a header at that height * @return true iff the ordering is acceptable for a header at that height
*/ */
public static boolean hasAcceptableIndices( public static boolean hasAcceptableIndices(
final Collection<FalconSeal> certificate, final long blockNumber) { final long chainId, final Collection<FalconSeal> certificate, final long blockNumber) {
if (hasStrictlyIncreasingIndices(certificate)) { if (hasStrictlyIncreasingIndices(certificate)) {
return true; 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. * 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 * <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 * @param blockNumber the height being judged
* @return true iff the height falls inside a range where nothing was in force * @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) { for (final Window window : WINDOWS) {
if (window.covers(blockNumber) && window.relaxation() == Relaxation.EVERYTHING) { if (window.covers(blockNumber) && window.relaxation() == Relaxation.EVERYTHING) {
return true; 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 * 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. * 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 * @param blockNumber the height being judged
* @return the threshold in force, or empty when the configured schedule applies unchanged * @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) { for (final Window window : WINDOWS) {
if (window.covers(blockNumber) && window.relaxation() == Relaxation.THRESHOLD_ONLY) { if (window.covers(blockNumber) && window.relaxation() == Relaxation.THRESHOLD_ONLY) {
return OptionalInt.of(window.effectiveMinSeals()); return OptionalInt.of(window.effectiveMinSeals());
@ -357,10 +369,14 @@ public final class PqAnchorLapse {
/** /**
* The range covering a height, when there is one. * 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 * @param blockNumber the height being judged
* @return the range, or empty * @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(); return WINDOWS.stream().filter(w -> w.covers(blockNumber)).findFirst();
} }

View File

@ -93,20 +93,20 @@ public class PqAnchorLapseTest {
@Test @Test
public void isDisarmedIsTrueOnlyWhereNothingWasInForce() { public void isDisarmedIsTrueOnlyWhereNothingWasInForce() {
assertThat(PqAnchorLapse.isDisarmed(DISARMED_FIRST)).isTrue(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST)).isTrue();
assertThat(PqAnchorLapse.isDisarmed(DISARMED_LAST)).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. // 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(PqAnchorLapse.CHAIN_ID, LAST_ENFORCED_BEFORE)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(DISARMED_FIRST - 1L)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST - 1L)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(DISARMED_LAST + 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 // 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. // relaxation that leaked into it would throw away the binding on 19,425 anchor heights.
assertThat(PqAnchorLapse.isDisarmed(LOWERED_FIRST)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(13_500_016L)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 13_500_016L)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(LOWERED_LAST)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, LOWERED_LAST)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(FIRST_FULLY_ENFORCED_AFTER)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, FIRST_FULLY_ENFORCED_AFTER)).isFalse();
} }
@Test @Test
@ -115,7 +115,7 @@ public class PqAnchorLapseTest {
for (long height = DISARMED_FIRST; for (long height = DISARMED_FIRST;
height <= DISARMED_LAST; height <= DISARMED_LAST;
height += PqAnchorLapse.ANCHOR_SPACING) { height += PqAnchorLapse.ANCHOR_SPACING) {
assertThat(PqAnchorLapse.isDisarmed(height)) assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, height))
.describedAs("anchor height %d of the disarmed range", height) .describedAs("anchor height %d of the disarmed range", height)
.isTrue(); .isTrue();
covered++; covered++;
@ -123,11 +123,11 @@ public class PqAnchorLapseTest {
assertThat(covered).isEqualTo(36L); assertThat(covered).isEqualTo(36L);
// The rest of the chain is untouched. // The rest of the chain is untouched.
assertThat(PqAnchorLapse.isDisarmed(0L)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 0L)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(13_014_000L)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 13_014_000L)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(13_000_000L)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 13_000_000L)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(14_077_000L)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, 14_077_000L)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(Long.MAX_VALUE)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, Long.MAX_VALUE)).isFalse();
} }
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
@ -136,23 +136,23 @@ public class PqAnchorLapseTest {
@Test @Test
public void historicMinSealsAnswersOnlyInsideTheLoweredRange() { public void historicMinSealsAnswersOnlyInsideTheLoweredRange() {
assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST)).hasValue(1); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST)).hasValue(1);
assertThat(PqAnchorLapse.historicMinSeals(13_500_016L)).hasValue(1); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, 13_500_016L)).hasValue(1);
assertThat(PqAnchorLapse.historicMinSeals(LOWERED_LAST)).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 // 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. // the assertion that stops the lowered threshold from becoming permanent.
assertThat(PqAnchorLapse.historicMinSeals(FIRST_FULLY_ENFORCED_AFTER)).isEmpty(); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, FIRST_FULLY_ENFORCED_AFTER)).isEmpty();
assertThat(PqAnchorLapse.historicMinSeals(LOWERED_LAST + 1L)).isEmpty(); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOWERED_LAST + 1L)).isEmpty();
assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST - 1L)).isEmpty(); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST - 1L)).isEmpty();
assertThat(PqAnchorLapse.historicMinSeals(LAST_ENFORCED_BEFORE)).isEmpty(); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LAST_ENFORCED_BEFORE)).isEmpty();
assertThat(PqAnchorLapse.historicMinSeals(14_077_000L)).isEmpty(); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, 14_077_000L)).isEmpty();
assertThat(PqAnchorLapse.historicMinSeals(Long.MAX_VALUE)).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 // 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. // threshold would be a second, contradictory way of saying the same thing.
assertThat(PqAnchorLapse.historicMinSeals(DISARMED_FIRST)).isEmpty(); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST)).isEmpty();
assertThat(PqAnchorLapse.historicMinSeals(DISARMED_LAST)).isEmpty(); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, DISARMED_LAST)).isEmpty();
} }
@Test @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 // 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, // therefore accept an empty certificate at a height where a certificate was in fact required,
// which is weaker than the history needs. // 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(); assertThat(PqAnchorLapse.windows().get(1).effectiveMinSeals()).isPositive();
} }
@Test @Test
public void windowCoveringNamesTheRangeOrNothing() { public void windowCoveringNamesTheRangeOrNothing() {
assertThat(PqAnchorLapse.windowCovering(DISARMED_FIRST)).isPresent(); assertThat(PqAnchorLapse.windowCovering(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST)).isPresent();
assertThat(PqAnchorLapse.windowCovering(DISARMED_FIRST).get().lastBlock()) assertThat(PqAnchorLapse.windowCovering(PqAnchorLapse.CHAIN_ID, DISARMED_FIRST).get().lastBlock())
.isEqualTo(DISARMED_LAST); .isEqualTo(DISARMED_LAST);
assertThat(PqAnchorLapse.windowCovering(LOWERED_FIRST)).isPresent(); assertThat(PqAnchorLapse.windowCovering(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST)).isPresent();
assertThat(PqAnchorLapse.windowCovering(LOWERED_FIRST).get().relaxation()) assertThat(PqAnchorLapse.windowCovering(PqAnchorLapse.CHAIN_ID, LOWERED_FIRST).get().relaxation())
.isEqualTo(PqAnchorLapse.Relaxation.THRESHOLD_ONLY); .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); 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)); 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. // Inside the window: accepted, at both edges and in the middle.
assertThat(PqAnchor.hasAcceptableIndices(real1, PqAnchor.UNORDERED_WINDOW_FIRST)).isTrue(); assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real1, PqAnchor.UNORDERED_WINDOW_FIRST)).isTrue();
assertThat(PqAnchor.hasAcceptableIndices(real2, PqAnchor.UNORDERED_WINDOW_LAST)).isTrue(); assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, 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 + 32L)).isTrue();
// NEGATIVE CONTROL, and it is the whole point: one block on either side of the window and the // 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 // very same certificate is rejected. If these two ever pass, the exception has stopped being an
// exception and has become the rule. // exception and has become the rule.
assertThat(PqAnchor.hasAcceptableIndices(real1, PqAnchor.UNORDERED_WINDOW_FIRST - 1L)).isFalse(); assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real1, PqAnchor.UNORDERED_WINDOW_FIRST - 1L)).isFalse();
assertThat(PqAnchor.hasAcceptableIndices(real1, PqAnchor.UNORDERED_WINDOW_LAST + 1L)).isFalse(); assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real1, PqAnchor.UNORDERED_WINDOW_LAST + 1L)).isFalse();
assertThat(PqAnchor.hasAcceptableIndices(real2, 1L)).isFalse(); assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real2, 1L)).isFalse();
assertThat(PqAnchor.hasAcceptableIndices(real2, Long.MAX_VALUE)).isFalse(); assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, real2, Long.MAX_VALUE)).isFalse();
} }
@Test @Test
@ -199,24 +199,24 @@ public class PqAnchorTest {
// unrepresentable, which is the only part of the ordering rule that an attacker cares about. // unrepresentable, which is the only part of the ordering rule that an attacker cares about.
final List<FalconSeal> repetat = final List<FalconSeal> repetat =
Arrays.asList(new FalconSeal(2, signatureFor(2, 0)), new FalconSeal(2, signatureFor(2, 1))); 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(PqAnchorLapse.CHAIN_ID, 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_LAST)).isFalse();
// Negative indices and null signatures stay rejected inside the window too. // Negative indices and null signatures stay rejected inside the window too.
assertThat( assertThat(
PqAnchor.hasAcceptableIndices( PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID,
List.of(new FalconSeal(-1, Bytes.of(1))), PqAnchor.UNORDERED_WINDOW_FIRST)) List.of(new FalconSeal(-1, Bytes.of(1))), PqAnchor.UNORDERED_WINDOW_FIRST))
.isFalse(); .isFalse();
assertThat( assertThat(
PqAnchor.hasAcceptableIndices( PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID,
List.of(new FalconSeal(1, null)), PqAnchor.UNORDERED_WINDOW_FIRST)) List.of(new FalconSeal(1, null)), PqAnchor.UNORDERED_WINDOW_FIRST))
.isFalse(); .isFalse();
// And a sorted certificate is accepted everywhere, window or not: the exception only ever adds. // 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)); final List<FalconSeal> sortat = Arrays.asList(seal(0), seal(3), seal(6));
assertThat(PqAnchor.hasAcceptableIndices(sortat, 1L)).isTrue(); assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, sortat, 1L)).isTrue();
assertThat(PqAnchor.hasAcceptableIndices(sortat, PqAnchor.UNORDERED_WINDOW_FIRST)).isTrue(); assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, sortat, PqAnchor.UNORDERED_WINDOW_FIRST)).isTrue();
assertThat(PqAnchor.hasAcceptableIndices(sortat, Long.MAX_VALUE)).isTrue(); assertThat(PqAnchor.hasAcceptableIndices(PqAnchorLapse.CHAIN_ID, sortat, Long.MAX_VALUE)).isTrue();
} }
@Test @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. // on-chain pre-image, so this is the same hash the parent header will carry once imported.
private Hash pqOnchainHash; 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. * Instantiates a new Qbft round.
* *
@ -429,10 +449,12 @@ public class QbftRound {
LOG.warn("Failed to construct commit seal; {}", e.getMessage()); LOG.warn("Failed to construct commit seal; {}", e.getMessage());
return true; return true;
} }
final Optional<FalconSeal> falconSeal = falconSealFor(block, commitHash); // AERE HYBRID: the Falcon seal and the other schemes' extras, over the SAME message, signed
// AERE HYBRID: the other schemes' extras, over the SAME message; empty on any node today. // 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 = 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. // There are times handling a proposed block is enough to enter prepared.
if (wasPrepared != roundState.isPrepared()) { if (wasPrepared != roundState.isPrepared()) {
@ -487,10 +509,12 @@ public class QbftRound {
try { try {
final Hash commitHash = commitHashFor(block); final Hash commitHash = commitHashFor(block);
final SECPSignature commitSeal = nodeKey.sign(Bytes32.wrap(commitHash.getBytes())); final SECPSignature commitSeal = nodeKey.sign(Bytes32.wrap(commitHash.getBytes()));
final Optional<FalconSeal> falconSeal = falconSealFor(block, commitHash); // AERE HIBRID: aceleasi sigilii si pe drumul tarziu, ca cele doua locuri sa nu divearga; D-337:
// AERE HIBRID: aceleasi extrase si pe drumul tarziu, ca cele doua locuri sa nu divearga. // 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 = 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()) { if (!extraSeals.isEmpty()) {
transmitter.multicastCommit( transmitter.multicastCommit(
getRoundIdentifier(), block.getHash(), commitSeal, falconSeal, extraSeals); 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 * hybrid-configured, and below the emission gate: {@link HybridSealProducer} never throws and
* never emits half a certificate. * 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) { final QbftBlock block, final Hash commitHash) {
// ANCHOR PARENTS ONLY (2026-09-03, D-329): an SLH-DSA-128s signature costs seconds here; it is // 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 // 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 extras are signed only when the block being committed is the parent of an anchor height.
// The same predicate gates the enforcement (PqCommitEnforcement.anchorParentByConfig). // The same predicate gates the enforcement (PqCommitEnforcement.anchorParentByConfig).
final long number = block.getHeader().getNumber(); final long number = block.getHeader().getNumber();
if (!org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.config() if (!pqExtrasApplyAt(number)) {
.anchorAppliesAt(number + 1L)) {
return java.util.List.of(); return java.util.List.of();
} }
return hybridExtrasOrEmpty(number, pqSealMessageFor(block, commitHash)); 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 * 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 * 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); 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 // 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. // 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 String ERROR_PREFIX = "Invalid RoundChange Payload";
private static final Logger LOG = LoggerFactory.getLogger(RoundChangePayloadValidator.class); 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 Collection<Address> validators;
private final long chainHeight; private final long chainHeight;
private final PqRoundChangeEnforcement pqEnforcement; private final PqRoundChangeEnforcement pqEnforcement;
@ -109,8 +112,8 @@ public class RoundChangePayloadValidator {
} }
final int targetRound = payload.getRoundIdentifier().getRoundNumber(); final int targetRound = payload.getRoundIdentifier().getRoundNumber();
if (targetRound <= 0) { if (targetRound <= 0 || targetRound > MAX_ALLOWED_ROUND) {
LOG.info("{}: must contain a positive target round number", ERROR_PREFIX); LOG.info("{}: target round number out of range [1, {}]", ERROR_PREFIX, MAX_ALLOWED_ROUND);
return false; 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 // 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 // measured, and why an exception written anywhere other than validation moves the error instead
// of ending it. // of ending it.
if (PqAnchorLapse.isDisarmed(number)) { if (PqAnchorLapse.isDisarmed(config.chainId(), number)) {
return acceptAsWrittenInsideLapse(header, 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 // 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 // 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. // 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 // 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 // in LIGHT validation too, and so that "the order given" and "sorted" are the same list for
// every header this rule accepts. // 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 " + "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.", + "that range has, and it is the one property the exception does not relax.",
number, number,
PqAnchorLapse.windowCovering(number).orElse(null)); PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null));
return false; return false;
} }
LOG.debug( LOG.debug(
"AERE PQ ANCHOR (R1): block {} accepted as written; it falls inside the historical range " "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.", + "{} in which the anchor rules were not in force, so it carries no digest to bind.",
number, number,
PqAnchorLapse.windowCovering(number).orElse(null)); PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null));
return true; return true;
} catch (final Exception e) { } catch (final Exception e) {
LOG.warn( 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 // 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, // 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. // so neither can be required of it now.
if (PqAnchorLapse.isDisarmed(number)) { if (PqAnchorLapse.isDisarmed(config.chainId(), number)) {
return acceptAsWrittenInsideLapse(header, 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 // 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. // range supports, and the ceiling is a measured floor, never zero. See PqAnchorLapse.
int required = config.minSealsAt(number); 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) { if (inForce.isPresent() && inForce.getAsInt() < required) {
LOG.debug( LOG.debug(
"AERE PQ ANCHOR (R2): block {} sits in the historical range {}, so the threshold " "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.", + "applied is the {} that was in force then, not the {} the schedule asks for.",
number, number,
PqAnchorLapse.windowCovering(number).orElse(null), PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null),
inForce.getAsInt(), inForce.getAsInt(),
required); required);
required = inForce.getAsInt(); 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 // 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 // PqAnchor.hasAcceptableIndices for why the exception sits at VALIDATION and nowhere else, and
// for what it deliberately does NOT relax. // for what it deliberately does NOT relax.
if (!PqAnchor.hasAcceptableIndices(certificate, number)) { if (!PqAnchor.hasAcceptableIndices(config.chainId(), certificate, number)) {
LOG.warn( LOG.warn(
"AERE PQ ANCHOR (R2): block {} REJECTED - the certificate's validator indices are not " "AERE PQ ANCHOR (R2): block {} REJECTED - the certificate's validator indices are not "
+ "strictly increasing.", + "strictly increasing.",
@ -536,7 +536,7 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
+ "which the anchor rules were not in force, but its certificate repeats a " + "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.", + "validator index or carries a negative one, which no header of that range does.",
number, number,
PqAnchorLapse.windowCovering(number).orElse(null)); PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null));
return false; return false;
} }
LOG.debug( LOG.debug(
@ -544,7 +544,7 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
+ "historical range {} in which neither the threshold nor the ordering was in force.", + "historical range {} in which neither the threshold nor the ordering was in force.",
number, number,
certificate.size(), certificate.size(),
PqAnchorLapse.windowCovering(number).orElse(null)); PqAnchorLapse.windowCovering(config.chainId(), number).orElse(null));
return true; return true;
} catch (final Exception e) { } catch (final Exception e) {
LOG.warn( LOG.warn(

View File

@ -139,12 +139,12 @@ public class PqAnchorDisarmedWindowTest {
.describedAs("the schedule asks for three seals at height %d", height) .describedAs("the schedule asks for three seals at height %d", height)
.isEqualTo(3); .isEqualTo(3);
} }
assertThat(PqAnchorLapse.isDisarmed(OFF_FIRST)).isTrue(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, OFF_FIRST)).isTrue();
assertThat(PqAnchorLapse.isDisarmed(OFF_LAST)).isTrue(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, OFF_LAST)).isTrue();
assertThat(PqAnchorLapse.isDisarmed(BEFORE)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, BEFORE)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(LOW_FIRST)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, LOW_FIRST)).isFalse();
assertThat(PqAnchorLapse.historicMinSeals(LOW_FIRST)).hasValue(1); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, LOW_FIRST)).hasValue(1);
assertThat(PqAnchorLapse.historicMinSeals(AFTER)).isEmpty(); assertThat(PqAnchorLapse.historicMinSeals(PqAnchorLapse.CHAIN_ID, AFTER)).isEmpty();
} }
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
@ -383,9 +383,9 @@ public class PqAnchorDisarmedWindowTest {
// exception having leaked one block down. // exception having leaked one block down.
assertThat(live.anchorAppliesAt(OFF_FIRST - 1L)).isFalse(); assertThat(live.anchorAppliesAt(OFF_FIRST - 1L)).isFalse();
assertThat(live.anchorAppliesAt(OFF_LAST + 1L)).isFalse(); assertThat(live.anchorAppliesAt(OFF_LAST + 1L)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(OFF_FIRST - 1L)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, OFF_FIRST - 1L)).isFalse();
assertThat(PqAnchorLapse.isDisarmed(OFF_LAST + 1L)).isFalse(); assertThat(PqAnchorLapse.isDisarmed(PqAnchorLapse.CHAIN_ID, OFF_LAST + 1L)).isFalse();
assertThat(PqAnchorLapse.historicMinSeals(LOW_LAST + 1L)).isEmpty(); 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(); 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. // Helpers.
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
@ -460,6 +521,19 @@ public class PqAnchorDisarmedWindowTest {
return header(number, parent.getHash(), extraData(digest, sorted)); 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) { 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 // 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 // errorprone's MixedMutabilityReturnType refuses, and it is refusing something real: the empty