diff --git a/anchor/BASE.txt b/anchor/BASE.txt
index 2707fd6..55be9da 100644
--- a/anchor/BASE.txt
+++ b/anchor/BASE.txt
@@ -5,6 +5,11 @@ anything else they overwrite, and silently delete, whatever upstream added in be
against a newer base, 26.7.1: 97 lines would be lost, among them the registration of the /liveness
and /readiness probes in BesuCommand.java, and nothing would fail.
+Since 2026-10-02 this directory holds every source file that the published patches (patches/0001 and
+0003 to 0008, in the order run-node.sh applies them) change or add, taken from the recipe itself:
+the base tree with those patches applied. It is a reading copy; the patches remain the authoritative
+form, and a check in the operator repository compares the two file by file.
+
IF YOU MOVE THE BASE, do not take whole files from here. Re-apply the same changes as a patch
against your own base: a patch fails loudly when upstream has moved, a whole file does not.
diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt
index d69b3c7..f4e5679 100644
--- a/anchor/MANIFEST-sha256.txt
+++ b/anchor/MANIFEST-sha256.txt
@@ -16,6 +16,7 @@ ed2e46aaf2e74661ad831dc31df0b6f795f28f4a94496a7d226feedf2a770fde consensus/comm
a6c2bbf05eab4173a749c67ef8b7bce6d184d8eac1b445acb847efd082dc6d62 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTags.java
290d94bb50f23666b131ec19659982c15f1e6cbaf8f8bfe128df9acb2f2f79fd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqHeaderSeal.java
348135b2e4d0a6a8ce91489c94fbfa77da2536196c13823676b33ea9d08f8fb9 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqIdentity.java
+dfdb28fb2d5016f0cbb7e8ba2cd9ababf18c6530349ddad3bf1e615cb25cc703 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockHashing.java
c531f9e719c48d3d5da2800fdef4506bd167af4dad4e25af96b4353a4390067d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java
9fcf44789ddf6d5b80be7d44f0bec58af6ba8316733d49817e890f0e17b71223 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java
70fa067469a084ac225765da6a01619dec51130acc255b7194a1ad274d327141 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java
@@ -37,16 +38,22 @@ f67a205036550f7d0f1bfed2abe7b89abd73e80614adff8d696b33c580269a6f consensus/comm
660ba13ad60c49e86cf9ee4ced2b6932a5422d988f85c5c23098a92b029f4d25 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java
11f1c2a97a40df999471977cb3eab9af4e816081b100e345e8c962ef3bbf76f5 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryV3.java
2ee139d21f73ff3417f6ad61b233e5ca3a95f92e68a4284eb99a47b1f8f4b58b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSchemeSchedule.java
-c0c93a096f4eceab6bf92cc3618ce7cee1e54d2d20b6ac781850a390ec6e514f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java
+80104994f8e5f02353c4bcb25084c451fd422c51c49e6891b574d76c7adf135f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java
73abbaddcec1533a7a758f1c4a26ae68a83ddbb96fd98ad147e74d7386bbbf3f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java
+776d9a422bcd306ad92d561ddebbbc1835d4ef5b147aa19d781556298bb27be4 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowStore.java
fad72fb52b6f6198113b80f5f2729002066a10decf596cfe26e602b058d9c603 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealed.java
a235b3ec8c69665c525d7c74a084d589904879f5cd33cb87e1d1f38eabba8a15 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java
addba40c0d931a3ecfa3b2f0179311dacaa604884e3ebb1d79f958bd913f72e2 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SchemeSeal.java
285f6a7c1188a387fb1ac21f5ab809595ef48d936b40aced94047525e6def0a6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java
3b57840dc103063b3797697feb78b90af93ca123587e9278cfb43b84edbd995a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealSchemes.java
444961503d515cb9f045729577e190941e59baa4bd90438a2da05f41fed7750a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SlhDsaSealScheme.java
+9021d608f62e1330cf9b421013a06f2141bae469ba9162a24ce66f749aa4b21a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreator.java
454eeca487505e70c7f0000f5401b6bf4530da1d5ce6bcb803427f1d08e3cb1c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java
+610275a18c2519d84ffb91982d7b9006a42605bbb330071572859df49f183c2a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftMiningCoordinator.java
e89deb6f346c221affa717eafa464d497401b7b5a01c84c144dace42032879a9 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java
+d8efb4d625233e18ef4764eaa29650a97b56e75af18d07607eda1dd484187f76 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/messagewrappers/BftMessage.java
+4b0731ec053e313a9ea9afc78592f25b6e97e6d230ebb273532437f08da92e5a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/network/ValidatorPeers.java
+478f9be60c1259481bfd7b5a687e23ada29b4b07d4cdf6f9d3bbc13584d384f8 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/payload/SignedData.java
1e0113154d79a91a07104bcc67a9a843847751b921c5f9b7cc96d7bc30a1452f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
15ab42221f35aeb0dfd468bcb840254f08ccfccb5ff36fba559ac1017beca5ee consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
51b7d889188793a307bccb297d983f9318548e6066ad6228e37170585eda665f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
@@ -71,6 +78,8 @@ fa85313f90f2e92b338d9c3f6df4cb57a7583fe8c82417f19928a133932dfd18 consensus/comm
2cd85f1517fe9770558ffef6251600bce76609a96beb21f0a8f120412b2a935f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java
6ace00e18914a1558563e689b7427654b5e6226a31447bef1d0416d993f78840 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java
ed1cc6f617dc5a2f7801b6e8f6f039a035a2ffc9542b116262dac39bb70563dd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryV3Tool.java
+8aff8079c93d1463111e303831dbef886568790e2cff8ade77801a2864ae355b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTally.java
+5a1014676efeea6e29d0603b4e7dac2d7334840c2f1ba3885bdc1b13e337cd7c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTallyUpdater.java
cf0dcc008ce2d040590329088abdcbfc0829bf529445422deee8022d4856e08a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelayTest.java
f5d6d26b9cd3babad8f7d16b9595497908b1f854d166837fbecd485d721ae856 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereKeysFromV3Test.java
1d7e3a30d3f4653b778a1bcbb81203ed8855d904017fa438a06e26baad8958c3 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereLocalKeysV3Test.java
@@ -108,6 +117,7 @@ f260d284f936307ac4c31142cbea0359ded1197880b8a960ac27dd7beb5c3b53 consensus/comm
05423fc69ce0e4e94cda0d56a181a996320bd2166af3415f2d88102cf3b0ae21 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryV3Test.java
d7d675be2a5ee598ce74f58156383589a05b8e977c2cc790671e3a39c7728309 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java
e73a075b2acfce7a6d2de63130ed55a3d8f422bdc72c51325a010852064d406d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java
+45f25de86a064b8fd8e16a2c2f2194fabe55b3fefaadfcdaad6e58e96c72c800 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowPersistenceTest.java
e0bc7a465dbb2dd7efa5f1830211a1a45c6554c300b2088dd70bb9677b0ac217 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java
9a371ec367cda846640c6da18c8bf7d921a6882cfc024dc0c86d4f965cae0c63 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java
e65ba92e288aebe768909b1fee5c0b8850be74b3658c6242e6abdb91409a9521 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqV2Fixture.java
@@ -120,6 +130,8 @@ ae37d3525697bf84defd796bb8813193c78f81e7fa24a72f8f048285d52399e2 consensus/comm
d0ba00d95427ae4fb1c0a1eeb37bcd039fbfa5412957dc4b2da0725c0dea5028 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SlhDsaParallelSigningTest.java
bcdd5c3c267f38eee9700bc6ba07d41b487c7889224820868e1839044108451f consensus/common/src/test/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTallyIdentityTest.java
6b6ecfcfb9b25bb009cb5d4df74648c729abf62626e5034af126771825b7418e consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Commit.java
+5ed47d976d4aafbb55369e1b3e96e6ad5efc3bf7c68be99ddb0e0ef9dbbc2f32 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Proposal.java
+394c557de273fa9fa9a5ad024e8a1982575ad756736aa045ffeb0bdc7dc8016c consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/RoundChange.java
a1405402e891189909a436abd9f83c863349fdcf01d3011542d805e2bb3911ed consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java
35d38accf0d22b6fdadeab4f2bce0cdac708f8115591594b1bb1fa385b227c5c consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java
536375fa2d9f27107d9907f34391413e052f2a8cd89611033fefe29b9edc7891 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java
@@ -128,7 +140,8 @@ 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
-80613a8ac11ab48dfcecce5690a28da7d66dd8332584fd221f2875cb33d8c9d0 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
+4a87188f209904c19f2b393fd50559a05b2be3161b4582364bc1641002bb4dff consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
+324a5607313f25adb377d633be1f221588f8dafaa47a3c9d1e84fc5697323090 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRoundFactory.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
@@ -141,6 +154,7 @@ afb487f8c1dd87e323fdc1153ecfdf8ec6cb5a10e0726783d4214c07a3fa9a95 consensus/qbft
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
afa427cea95acd4d1f54f6824d39fae4125a7d4e23a84785f10b84ae9fe756ad consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidator.java
+a32033398fd0a680f5d47930bbc7910e9eb178cdbc091419c35cf2a17e04ec27 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/AereD434ListBoundsTest.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
@@ -176,6 +190,7 @@ dc9f9e862a11f0135d26976176a1d1adac3e84fd5f1e06572d8727262cb3374f consensus/qbft
24ee993a415eea57c80fe2c9b839ce20140bfcc580e269e7b1182f71e960c1f0 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java
ebf6da56130720ea95c64505959d64bfba46540ebf2139c3f71a7051d88837a1 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java
c870d39a19174328bc394658f4a365a61bd57fe7b0e28e6c5e4aaeb28bb509e5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java
+1238bdf6968781d12c7c7da8d31e5cf8b281a3e09612d450dc8ebae208542b31 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftFinalStateImpl.java
cfb0aad408ab620f28b09914ece7d8d3d09f9e6bbb503070e28d9ef39c023dee consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/blockcreation/QbftBlockCreatorFactory.java
cbec6f4e280250ddefe5c79c453b59a07be6435e6f88d5614e92168c855e4693 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRule.java
e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRule.java
@@ -186,6 +201,8 @@ a76a87987050ea6765e2a1f43d8f828ca8aec218c951e395e7acb62465e8b2fd consensus/qbft
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
8ac99e94c89f61f0281199cf369e282fc4f9fb1a3414a1650766e1d7548e76f5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java
+99cc2db4894a35c1df3c10d52d0deeffc4d6bf2a219f07249f5baccedf3889e7 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java
+24b52131993fc93c5f146cab8588067cdb6201e308c36eef455d0ea56460a13b consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinality.java
5dcf5c22389c063e12363b83366b39bd3b2e79f36c68e777fb3ef11080f53ea9 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/AereIdentityHeaderTest.java
52b56f157500ae3527b7e55c51786915bb5d6980065668f1e7b8e297bf7125ef consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftAnchorRuleWiringTest.java
5a18c7fee308654d9557fc507ca7a64704bdac5d13835e7df47a3dfb41519902 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRuleTest.java
@@ -201,12 +218,72 @@ a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft
484bc74ff3d1e52d25631264d83af70c2b695e973c7360b0f3db471ab5c6613a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRuleTest.java
49c3fd97e7985966531928af173b6f38ac48ff9dc8b5be1106e5b7bfb9c7b2b6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqForkGateFeedTest.java
8d0113eec652dc5e08600fb7fb07de3e08524a500ea6bdcf3fd0f46f3f1b38e6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java
-7ea99f24bc1c9131e8ef08f7a2c6f2a08af71ebee011a4cb88a367bd446b6c27 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java
-5bf41ab0be06328fa1e18bf9c98bfd89b7b16384fe185d96b3385595dabe61b3 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetCode.java
-4c96b06073ff3830f4e0292fc732ec3fb9e0ed50ef741dceedf755b50f18d5ae ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetStorageAt.java
-8e649d2a1c47cd912f0782408da3f3af5537e8c670c12cba72dbb502809c848d ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionCount.java
-41b5f7b0066ef6d8af25491814eeacf2bcbb29415f407020affde05b7d1916b7 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/query/BlockchainQueries.java
+212e932427839cebf788cf90ffb7893f2905880219bbcd051ee0b9279810df35 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinalityTest.java
+f76961f1c07629cd4d3061ba3b59167875cf517d7adc7ceb302894ef430baf52 datatypes/src/main/java/org/hyperledger/besu/datatypes/Address.java
+cd815f46bd834f62f49551dd0dc44b9c0cf88d33311fc5928803949dc9964562 datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAlgorithm.java
+d3658d8ccc923f1b890b80fa83673c15cc213615db0ac2a9e343cd18e931c471 datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAuthorization.java
+17560a53ed57b6c22b43a6223eb0f52289eb1565dad2c1fe4a8a5565426a07cf datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java
+9858c5116ce32ee4ebfa247a5eaa2befe1f5ec6507f61c7115f04a49ba0ece5f datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java
+9e370c961785ad6a2b16b3fc377a5341a5e930136bf9a47a8e1079ae501d1b62 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java
+4f2fe5b68e55276d62181a8694fc8dbd44f4680ca5c0a28a555425135563cc43 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java
+b5394c8f60a8afc46c0e09b8198b9212e808c728050bc20808e4652d45019d39 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetCode.java
+dfceadc9d220ef9b028ef30e09e12a2c44caffee1922b6aa98c95937862bc9f9 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetStorageAt.java
+cd94a4c059dd5e79136544f9dc09163df722ecedd41b8552cd0cdc22ba6328d3 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionCount.java
+ce03332291e5a575cede22e2885e561ae5d27e815551d7d4af3a93045949eabc ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionBaseResult.java
+6a54e53e6f1569b0bc00617188b8d82c6d69fa2de13db6285af0c5e959d1eae7 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/pending/PendingTransactionDetailResult.java
+2de0590a917378d7cd1cb2a4ab1a4f9d0ac27545c0e080a83417244718606ba5 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/query/BlockchainQueries.java
3ee087fc34b455e113ec9d2c53b5dbdb3574429fcf166f4c968a80120f70f0eb ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AereStateUnavailableIsAnErrorTest.java
6e99e9f399558abbd1d4fdb4b176ba413bf31cabb3c8aba1701c17ce5828ce8a ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/AereHeadStateRetryTest.java
+bf6617807ee0dd03ed98e6f1a73d6d11366880c2f742f34e73ef4d74a4f01180 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java
+5d5032dbdad0f3dd15539b3ee6676cf3fd2bb0381d0db17d8abea2051b5c2c09 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionDecoder.java
+95a30107a67e12d0f45df74365fd533bde15fab76036451590539a73714b0b84 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionEncoder.java
+53f750f62dbf256a3d25fab595d651dd3c3872c207659aa839ab8403df4cf2ad ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java
+2aa829c45cb5902f8467218244680ebc3f87162e6be8897b39d286813046dc23 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java
+b3f5feddcae2e74f3b2517785a51e61998817fba4977d5f5a3fb9fbb48038af8 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AerePqTransactionSupport.java
+10df69ba73a9847ea5b93d2fc2bc1c2dbf238b8b76d713568678403ea6edbd87 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java
+73f66fae03826866c74e87bd262e7bac79ce3db814de782dc52b9263ce156059 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java
+c19d72f93d09f945a6c5a0144125f8501cc0cb0d0749cd08e8d8ca091ad40dcc ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java
+f2257781bf894f96f0fd67bc8f5d97fb8df181a95029147a9e594b068ca04cf2 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/WithdrawalsValidator.java
+b3c6811983da5ce74f75a0fccdd87a2385881fa99210cdb0484d357ac0d844ab ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/AereBlockStmEngine.java
+20d842d8413034e506c6ab22288a9e6870f0fb8a83362ec5adecb70b49f6af07 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/AereBlockStmPreprocessing.java
+bd5d69c4ec63c212f364fa7fbb3ad658983654e772148bedb17c8028ce24540e ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/AereBlockStmProcessor.java
+fb4a98f4edf9e8c362ce241e44771f6c8db8c34f54b8e4efb026ba3747b3975e ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/BaseReader.java
+ae98ca304a11bfc7e051f5bfaf43b4270dca40dbae47f931e3d186358a87cb03 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/BlockStmStats.java
+85b13b2561f7541b6bb1187a2d6c07a8aafdd447bb04ad712fe540703f94f5a1 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/FlatBaseSnapshot.java
+cdc1bd5ce7bdeecc5328885622f7b21028c2d3d5bf4b60d5da13d8e7819dc984 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvAccount.java
+e74fcdcf9fa8874c0eaf7e4467be0a31351faf096bb4147fe0d8702068fa2bd6 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvBaseLayer.java
+e3fa43a8ac48028631d63d6e1a13f32c27a14430b52d1a1d89d9c2a454d954a3 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvContext.java
+6e83f1158908ac8262f5de81be72a3adbc7e35f5dce57db576437ba924fdd1e3 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvKey.java
+1e6dbfb45124b5cad58ee760fd394ccd12f8d514508eb533d6fbb1ee7c4f94b7 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvMemory.java
+a1422099123d6644e53129c9caa59bca7cb54669c5a07438f53bb8cd065c89b2 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/Scheduler.java
+26a80db3a78556e874f6176e82c5e86475ba55fb574660a8a856806479ccd1c9 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/feemarket/LondonFeeMarket.java
+10c888f52c388ec4ce8767ff435a70ef14c341eaa632acb10f8e75622e9ca3ae ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java
+cbd5d4e986818ad65259e2bf3c46b1fff1bbec96a5513212a1b2ef1448c18cb7 ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java
+f3ba58dda5335659912c7bf9c336ee00b2b5645ed7f868ffa9955425cb2ced7d ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/AerePqTransactionTest.java
+01ac1387d8a7d155a56274c2929b0f129053c78b2602137a7bdd00c8a99009f6 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AereD432GasCapUnsignedTest.java
+aa4999574b158ed3a9d738d7439033d543ed0a748792e0e60d1eb8ca8af84427 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AereD433BftWithdrawalsTest.java
+816c47aba04623a15e48ece16b440d6d9e16d23031841c22f9159830862d6dd6 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/feemarket/LondonFeeMarketAereFloorTest.java
+d34dd8c46ad4a394e8b15a2155c32f89883a0879c1cf7f1976f081938a3f56d3 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AereBlockStmBenchmarkTest.java
+6f0119bcafb5247995b4399f766d4adc9275669fe3ca08fbe2df5b01800624e3 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AereBlockStmSmokeTest.java
+f94acc1cd8aac6679c39c74f7d23375ba19781e9bfd1d14b8c8a9e4314384cc1 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeers.java
+62abf412b67808f0648b1af363a376e567ba7796cb1c4aa93617adc3f2484b8e ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/BlockPropagationManager.java
3b690b72e80a0f9eaf868a3fa3f99ace42dad74dc30b2c98b9f3ca5a023917c0 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadHeadersStep.java
7f611966d74f52ef2f6d55bb5882ffb72b10abf1985df2b01f03aad4a6e2baf1 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/PropagatedBlockAdoptionGate.java
+5a8187cc12bb1a16a9b77956c4e1c5d8c088a4e65164179c427cd7b2c2f24b7f ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/state/SyncState.java
+e068fc39b4f645d1ccdd778acdf77983869a73428513c11ce587547391e36ce3 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java
+eb8d2305070adcf0424ab9884c8f09d9bc95a1662a4cf8775a136e3a314fa181 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java
+76ce97047c7988c106969f69fbdcba6941657c482ea1e060b995e7ce0556c108 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/state/SyncStateTest.java
+cc003a07cc4cf27e20ebf249ae8d84b8f514d1c24e55dfaab00fea3c345e1a0c ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java
+55e6c022442a970411bb677a1d0b27a4a7f8174b4fabd200de5b10393731b201 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java
+e39a09c49cec66fa2766470ba29bbccced6f42ecb7f95df39ccf5086b666510c ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLPInput.java
+2190cd3df779c0b04ca1ca09098ea1a99df07dab6659a2d4cac058f34c27e495 evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java
+eeeb63160aba1f889725d628b8623630baf227a82e367844b6d24619d8e83e7d evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java
+72866e4744d34a120345a1ed3a0699292fd4a63d04d6256b3a7337738441e7bf evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java
+417134787ef4f01af0674bb8e641a7eef3c701e1d9d4e8a1c807faab4bb97d99 evm/src/main/java/org/hyperledger/besu/evm/precompile/AereFalconSupport.java
+c59d8c714155b347fe64bb378715e71670a8bd42180a97d08156e6c20caa359c evm/src/main/java/org/hyperledger/besu/evm/precompile/Falcon1024PrecompiledContract.java
+6487810e9b167eb6c0a7046a5226e40936116771efb91013d6c2eb9559c60f97 evm/src/main/java/org/hyperledger/besu/evm/precompile/Falcon512PrecompiledContract.java
+42e4d1442d3478eac8c3177e35318872c365a173d4a16f529356e52944901d50 evm/src/main/java/org/hyperledger/besu/evm/precompile/MLDSA44PrecompiledContract.java
+d17d9fde238d5d6527380d9829d1b52cd3c7c76c9874c6172e4ddf1dcc26acba evm/src/main/java/org/hyperledger/besu/evm/precompile/MainnetPrecompiledContracts.java
+c3b208adc9e81229eb9194fbedd40186aeccf43b8039827604d00169878d6690 evm/src/main/java/org/hyperledger/besu/evm/precompile/P256VerifyPrecompiledContract.java
+c4ef4c11f13c7387b79c5d6532299f540d8c4cb82c98a865be2d01053bd5d35e evm/src/main/java/org/hyperledger/besu/evm/precompile/SHAKE256PrecompiledContract.java
+976daf6471e817ca668942aa07adfc7ede7d59b4c941d0bbb91d02a25fa6fbb4 evm/src/main/java/org/hyperledger/besu/evm/precompile/SLHDSA128sPrecompiledContract.java
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockHashing.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockHashing.java
new file mode 100644
index 0000000..06cb610
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockHashing.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
+ */
+package org.hyperledger.besu.consensus.common.bft;
+
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.datatypes.Hash;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder;
+import org.hyperledger.besu.ethereum.core.Util;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
+
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.apache.tuweni.bytes.Bytes;
+
+/** The Bft block hashing. */
+public class BftBlockHashing {
+ private final BftExtraDataCodec bftExtraDataCodec;
+
+ /**
+ * Instantiates a new Bft block hashing.
+ *
+ * @param bftExtraDataCodec the bft extra data codec
+ */
+ public BftBlockHashing(final BftExtraDataCodec bftExtraDataCodec) {
+ this.bftExtraDataCodec = bftExtraDataCodec;
+ }
+
+ /**
+ * Constructs a hash of the block header suitable for signing as a committed seal. The extra data
+ * in the hash uses an empty list for the committed seals.
+ *
+ * @param header The header for which a proposer seal is to be calculated (with or without extra
+ * data)
+ * @param bftExtraData The extra data block which is to be inserted to the header once seal is
+ * calculated
+ * @return the hash of the header including the validator and proposer seal in the extra data
+ */
+ public Hash calculateDataHashForCommittedSeal(
+ final BlockHeader header, final BftExtraData bftExtraData) {
+ return Hash.hash(
+ serializeHeader(
+ header,
+ () -> bftExtraDataCodec.encodeWithoutCommitSeals(bftExtraData),
+ bftExtraDataCodec));
+ }
+
+ /**
+ * Calculate data hash for committed seal hash.
+ *
+ * @param header the header
+ * @return the hash
+ */
+ public Hash calculateDataHashForCommittedSeal(final BlockHeader header) {
+ final BftExtraData bftExtraData = bftExtraDataCodec.decode(header);
+ return Hash.hash(
+ serializeHeader(
+ header,
+ () -> bftExtraDataCodec.encodeWithoutCommitSeals(bftExtraData),
+ bftExtraDataCodec));
+ }
+
+ /**
+ * Constructs a hash of the block header, but omits the committerSeals and sets round number to 0
+ * (as these change on each of the potentially circulated blocks at the current chain height).
+ *
+ * @param header The header for which a block hash is to be calculated
+ * @return the hash of the header to be used when referencing the header on the blockchain
+ */
+ public Hash calculateHashOfBftBlockOnchain(final BlockHeader header) {
+ final BftExtraData bftExtraData = bftExtraDataCodec.decode(header);
+ return Hash.hash(
+ serializeHeader(
+ header,
+ () -> bftExtraDataCodec.encodeWithoutCommitSealsAndRoundNumber(bftExtraData),
+ bftExtraDataCodec));
+ }
+
+ /**
+ * Recovers the {@link Address} for each validator that contributed a committed seal to the block.
+ *
+ * @param header the block header that was signed by the committed seals
+ * @param bftExtraData the parsed {@link BftExtraData} from the header
+ * @return the addresses of validators that provided a committed seal
+ */
+ public List
recoverCommitterAddresses(
+ final BlockHeader header, final BftExtraData bftExtraData) {
+ final Hash committerHash = calculateDataHashForCommittedSeal(header, bftExtraData);
+
+ return bftExtraData.getSeals().stream()
+ .map(p -> AerePqIdentity.author(Util.signatureToAddress(p, committerHash), header.getNumber()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Serialize header bytes.
+ *
+ * @param header the header
+ * @param extraDataSerializer the extra data serializer
+ * @param bftExtraDataCodec the bft extra data codec
+ * @return the bytes
+ */
+ public static Bytes serializeHeader(
+ final BlockHeader header,
+ final Supplier extraDataSerializer,
+ final BftExtraDataCodec bftExtraDataCodec) {
+
+ // create a block header which is a copy of the header supplied as parameter except of the
+ // extraData field
+ final BlockHeaderBuilder builder = BlockHeaderBuilder.fromHeader(header);
+ builder.blockHeaderFunctions(BftBlockHeaderFunctions.forOnchainBlock(bftExtraDataCodec));
+
+ // set the extraData field using the supplied extraDataSerializer if the block height is not 0
+ if (header.getNumber() == BlockHeader.GENESIS_BLOCK_NUMBER) {
+ builder.extraData(header.getExtraData());
+ } else {
+ builder.extraData(extraDataSerializer.get());
+ }
+
+ final BytesValueRLPOutput out = new BytesValueRLPOutput();
+ builder.buildBlockHeader().writeTo(out);
+ return out.encoded();
+ }
+}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java
index dac0d63..716c67e 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java
@@ -69,11 +69,39 @@ public final class PqSealCache {
private static final Logger LOG = LoggerFactory.getLogger(PqSealCache.class);
- /** How many distinct block hashes are retained. Several rounds at one height each add one. */
- private static final int MAX_ENTRIES = 1024;
+ /**
+ * AIP-21 (AERE, 2026-09-18): how far below the highest seen height entries are kept, in blocks. The
+ * consensus default stays 256 (what the producer needs is the parent); a node that serves
+ * aere_getPqFinality may keep more through the system property, at ~7 kB per block for ten seals.
+ * Read once, never below 256, never a consensus value: two nodes with different windows import
+ * the same chain.
+ */
+ public static final String PROPERTY_FINALITY_WINDOW = "aere.pq.finality.window";
+
+ private static final long DEFAULT_HEIGHT_WINDOW = 256L;
/** How far below the highest seen height entries are kept, in blocks. */
- private static final long HEIGHT_WINDOW = 256L;
+ private static final long HEIGHT_WINDOW = readWindow();
+
+ /** How many distinct block hashes are retained. Several rounds at one height each add one. */
+ private static final int MAX_ENTRIES = (int) Math.max(1024L, Math.min(HEIGHT_WINDOW * 4L, 1 << 20));
+
+ private static long readWindow() {
+ final Long v = Long.getLong(PROPERTY_FINALITY_WINDOW);
+ if (v == null) {
+ return DEFAULT_HEIGHT_WINDOW;
+ }
+ return Math.max(DEFAULT_HEIGHT_WINDOW, Math.min(v, 1L << 18));
+ }
+
+ /**
+ * The retention window in blocks, as this node runs it.
+ *
+ * @return the window
+ */
+ public long retentionBlocks() {
+ return HEIGHT_WINDOW;
+ }
private static final PqSealCache INSTANCE = new PqSealCache();
@@ -90,6 +118,16 @@ public final class PqSealCache {
private Hash persistedHash;
private int persistedCount = -1;
+ // AIP-21, SECOND HALF (2026-09-18): the finality window survives a restart. Heard heights are
+ // appended to a segmented journal (PqSealWindowStore) when the NEXT height begins, and read back,
+ // re-verified seal by seal against the canonical chain, at startup. Null = journal off.
+ private volatile Path windowDirectory;
+ private volatile long windowPrunedSegment = -1L;
+ private long windowJournaledThrough = -1L;
+
+ /** Heights restored at startup at most, newest first, whatever the configured window. */
+ private static final long MAX_RESTORED_HEIGHTS = 4096L;
+
private PqSealCache() {}
/**
@@ -121,6 +159,9 @@ public final class PqSealCache {
}
this.persistenceChainId = chainId;
this.persistenceFile = PqSealStore.fileIn(dataDirectory);
+ this.windowPrunedSegment = -1L;
+ this.windowDirectory =
+ PqSealWindowStore.enabled() ? PqSealWindowStore.directoryIn(dataDirectory) : null;
LOG.info(
"AERE PERSISTENTA-SIGILII: heard seals for the chain head are kept in {} (chainId={}). "
+ "Every seal read back is re-verified against the anchored registry before it is used.",
@@ -133,6 +174,7 @@ public final class PqSealCache {
*/
public void disablePersistence() {
this.persistenceFile = null;
+ this.windowDirectory = null;
synchronized (writeLock) {
persistedBlock = -1L;
persistedHash = null;
@@ -226,6 +268,7 @@ public final class PqSealCache {
public void record(
final long blockNumber, final Hash onchainBlockHash, final Collection seals) {
final List toPersist;
+ List windowToJournal = List.of();
synchronized (this) {
if (onchainBlockHash == null || seals == null || seals.isEmpty()) {
return;
@@ -238,6 +281,9 @@ public final class PqSealCache {
}
}
if (blockNumber > highestSeen) {
+ // AIP-21 second half: a NEW height begins, so what was heard for the heights below it is as
+ // complete as this node will ever hold it. It goes to the window journal, outside this lock.
+ windowToJournal = windowSnapshotBelow(blockNumber);
highestSeen = blockNumber;
}
prune();
@@ -251,6 +297,8 @@ public final class PqSealCache {
// Deliberately OUTSIDE the map lock: a file write must never be able to stall a caller that only
// wants to read the cache.
persist(blockNumber, onchainBlockHash, toPersist);
+ // the head file first (liveness depends on it), the window journal after (best effort)
+ persistWindow(windowToJournal);
}
private void persist(
@@ -396,6 +444,7 @@ public final class PqSealCache {
public synchronized void clear() {
byHash.clear();
highestSeen = -1L;
+ windowJournaledThrough = -1L; // what clear() forgets, the journal mark forgets too
synchronized (writeLock) {
persistedBlock = -1L;
persistedHash = null;
@@ -403,6 +452,157 @@ public final class PqSealCache {
}
}
+ /**
+ * AIP-21, second half. Everything heard BELOW a height that has just begun and not yet handed to
+ * the window journal. Caller holds the map lock.
+ *
+ * @param newHeight the height that has just begun
+ * @return the heard blocks to journal, possibly empty
+ */
+ private List windowSnapshotBelow(final long newHeight) {
+ if (windowDirectory == null) {
+ return List.of();
+ }
+ final List out = new ArrayList<>();
+ for (final Map.Entry e : byHash.entrySet()) {
+ final long n = e.getValue().blockNumber;
+ if (n > windowJournaledThrough && n < newHeight && !e.getValue().seals.isEmpty()) {
+ out.add(PqSealWindowStore.heard(n, e.getKey().getBytes(), e.getValue().seals.values()));
+ }
+ }
+ windowJournaledThrough = Math.max(windowJournaledThrough, newHeight - 1L);
+ return out;
+ }
+
+ private void persistWindow(final List heard) {
+ final Path directory = windowDirectory;
+ if (directory == null || heard.isEmpty()) {
+ return;
+ }
+ synchronized (writeLock) {
+ try {
+ long highest = -1L;
+ for (final PqSealWindowStore.Heard h : heard) {
+ PqSealWindowStore.append(directory, persistenceChainId, h);
+ highest = Math.max(highest, h.blockNumber());
+ }
+ final long segment = highest / PqSealWindowStore.SEGMENT_BLOCKS;
+ if (segment != windowPrunedSegment) {
+ PqSealWindowStore.pruneSegments(directory, highest, HEIGHT_WINDOW);
+ windowPrunedSegment = segment;
+ }
+ } catch (final Exception e) {
+ // A disk fault must not be able to stop this node from taking part in consensus. It costs the
+ // answer aere_getPqFinality gives for these blocks after a restart, and nothing else.
+ LOG.warn(
+ "AERE FEREASTRA-SIGILII: could not append {} heard block(s) to the window journal {} ({}). "
+ + "This node keeps working; after a restart it answers 'unavailable' for them.",
+ heard.size(),
+ directory,
+ e.toString());
+ }
+ }
+ }
+
+ /**
+ * AIP-21, second half. Bring the finality window back after a restart. Nothing in the journal is
+ * believed: a frame counts only if its hash is the CANONICAL hash this node's own chain holds at
+ * that height, and each seal only if it verifies, against the registry epoch bound at that height,
+ * over the commit message rebuilt from that canonical hash. Never throws.
+ *
+ * @param headNumber this node's chain head
+ * @param canonicalHashAt the canonical on-chain hash at a height, empty when the chain holds none
+ * @param registry the anchored registry seals are verified against
+ * @return how many verified seals entered the window
+ */
+ public int restoreWindowFromDisk(
+ final long headNumber,
+ final java.util.function.LongFunction> canonicalHashAt,
+ final PqSignerRegistry registry) {
+ final Path directory = windowDirectory;
+ if (directory == null || canonicalHashAt == null || registry == null) {
+ return 0;
+ }
+ int restored = 0;
+ try {
+ final long from = Math.max(0L, headNumber - Math.min(HEIGHT_WINDOW, MAX_RESTORED_HEIGHTS));
+ final List frames =
+ PqSealWindowStore.readAll(directory, persistenceChainId, from, headNumber);
+ long highestInJournal = -1L;
+ int notCanonical = 0;
+ int rejected = 0;
+ final LinkedHashMap verified = new LinkedHashMap<>();
+ for (final PqSealWindowStore.Heard frame : frames) {
+ final long n = frame.blockNumber();
+ highestInJournal = Math.max(highestInJournal, n);
+ final java.util.Optional canonical = canonicalHashAt.apply(n);
+ if (canonical == null
+ || canonical.isEmpty()
+ || !canonical.get().getBytes().equals(frame.blockHash())) {
+ notCanonical++;
+ continue;
+ }
+ final org.apache.tuweni.bytes.Bytes32 message =
+ PqAnchor.commitMessage(persistenceChainId, n, frame.blockHash());
+ for (final FalconSeal seal : frame.seals()) {
+ if (seal == null
+ || seal.getValidatorIndex() < 0
+ || seal.getSignature() == null
+ || !registry.verifyAtHistoric(
+ n, seal.getValidatorIndex(), message, seal.getSignature())) {
+ rejected++;
+ continue;
+ }
+ final Entry entry = verified.computeIfAbsent(canonical.get(), unused -> new Entry(n));
+ if (entry.seals.putIfAbsent(seal.getValidatorIndex(), seal) == null) {
+ restored++;
+ }
+ }
+ }
+ synchronized (this) {
+ // Oldest first, so the size cap still evicts the oldest: the restored heights go in BEFORE
+ // what this node already holds (its head, restored a moment ago from the head file).
+ final LinkedHashMap held = new LinkedHashMap<>(byHash);
+ byHash.clear();
+ byHash.putAll(verified);
+ for (final Map.Entry e : held.entrySet()) {
+ final Entry target = byHash.get(e.getKey());
+ if (target == null) {
+ byHash.put(e.getKey(), e.getValue());
+ } else {
+ e.getValue().seals.forEach(target.seals::putIfAbsent);
+ e.getValue().extras.forEach(target.extras::putIfAbsent);
+ }
+ }
+ for (final Entry entry : verified.values()) {
+ if (entry.blockNumber > highestSeen) {
+ highestSeen = entry.blockNumber;
+ }
+ }
+ windowJournaledThrough = Math.max(windowJournaledThrough, highestInJournal);
+ prune();
+ }
+ LOG.info(
+ "AERE FEREASTRA-SIGILII: {} verified seal(s) over {} block(s) restored into the finality "
+ + "window from {} ({} frame(s) read, {} not canonical on this chain, {} seal(s) FAILED "
+ + "verification and were dropped).",
+ restored,
+ verified.size(),
+ directory,
+ frames.size(),
+ notCanonical,
+ rejected);
+ } catch (final RuntimeException e) {
+ LOG.warn(
+ "AERE FEREASTRA-SIGILII: restoring the finality window from {} failed unexpectedly ({}); "
+ + "the window starts empty, exactly as before this journal existed.",
+ directory,
+ e.toString());
+ return 0;
+ }
+ return restored;
+ }
+
private void prune() {
final Iterator> it = byHash.entrySet().iterator();
while (it.hasNext()) {
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowStore.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowStore.java
new file mode 100644
index 0000000..b224d8f
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowStore.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright contributors to Besu / AERE Network.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.besu.consensus.common.bft;
+
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
+import org.hyperledger.besu.ethereum.rlp.RLPInput;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.CRC32;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AIP-21, SECOND HALF (2026-09-18): the finality window on disk.
+ *
+ *
The first half gave every validator a window of heard Falcon seals ({@link PqSealCache}) and a
+ * method that reads it back ({@code aere_getPqFinality}). The window lived in memory only, so a node
+ * that restarted answered {@code unavailable} for every block it had heard before the restart. This
+ * class is the journal that carries the window across a restart.
+ *
+ *
SHAPE. One directory under the node's data directory, {@value #DIRECTORY_NAME}, holding
+ * SEGMENT files of {@value #SEGMENT_BLOCKS} heights each ({@code w-.rlp}). A segment is
+ * a sequence of FRAMES: {@code length(4, big endian) || payload || crc32(payload)(4)}, the payload
+ * being the RLP list {@code [DOMAIN, chainId, blockNumber, blockHash, certificate]} with the
+ * certificate encoded exactly as {@link PqSealStore} and the header encode it. Frames are only ever
+ * APPENDED, one per heard block, when the next height begins; whole segments below the window are
+ * deleted. The footprint is therefore bounded by the window, whatever the age of the node.
+ *
+ *
WHY THIS IS NOT A8 IN ANOTHER COAT, the same argument as {@link PqSealStore}: a seal is
+ * SELF-VERIFYING. Nothing read from this directory is believed. {@link PqSealCache} re-verifies
+ * every seal against the registry epoch bound at its height, over the message rebuilt from the
+ * CANONICAL hash this process reads from its own chain at that height; a frame for a hash that is
+ * not canonical is dropped before a single signature is looked at. A forged journal can obtain the
+ * empty window an absent journal already gives, and nothing else.
+ *
+ *
WHAT A FAULT COSTS. Every method here is written not to throw on bad input: a torn tail (the
+ * node died mid-append), a flipped byte, a directory where a file should be, a journal from another
+ * chain. A bad frame costs that frame and what FOLLOWS it in the same segment (a frame boundary
+ * cannot be trusted after a bad length), never the node. Writes are best effort and unsynced: the
+ * head file of {@link PqSealStore} is the one liveness depends on, and it keeps its fsync.
+ */
+public final class PqSealWindowStore {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PqSealWindowStore.class);
+
+ /** Directory name under the node's data directory. */
+ public static final String DIRECTORY_NAME = "aere-pq-seal-window";
+
+ /** Domain separator of a frame payload. */
+ public static final String DOMAIN = "AERE-PQ-SEALWINDOW-1";
+
+ private static final Bytes DOMAIN_BYTES = Bytes.wrap(DOMAIN.getBytes(StandardCharsets.US_ASCII));
+
+ /** Heights per segment file. */
+ public static final long SEGMENT_BLOCKS = 64L;
+
+ /** A segment larger than this is ignored whole. */
+ public static final long MAX_SEGMENT_BYTES = 64L * 1024L * 1024L;
+
+ /** A frame payload larger than this ends the reading of its segment. */
+ public static final int MAX_FRAME_BYTES = 4 * 1024 * 1024;
+
+ /** More seals than this in one frame is not a certificate. */
+ public static final int MAX_SEALS = 1024;
+
+ /** System property: {@code false} turns the journal off (the window is then memory only). */
+ public static final String PROPERTY_PERSIST = "aere.pq.finality.persist";
+
+ private static final Pattern SEGMENT_NAME = Pattern.compile("^w-(\\d{1,18})\\.rlp$");
+
+ private PqSealWindowStore() {}
+
+ /**
+ * One heard block as the journal holds it. Unverified: the caller verifies.
+ *
+ * @param blockNumber the height
+ * @param blockHash the round-independent on-chain hash the seals were heard for
+ * @param seals the heard seals, index-sorted
+ */
+ public record Heard(long blockNumber, Bytes blockHash, List seals) {}
+
+ /**
+ * Whether the journal is enabled for this process.
+ *
+ * @return false only when {@value #PROPERTY_PERSIST} is {@code false}
+ */
+ public static boolean enabled() {
+ return !"false".equalsIgnoreCase(System.getProperty(PROPERTY_PERSIST));
+ }
+
+ /**
+ * The journal directory of a node.
+ *
+ * @param dataDirectory the node's data directory
+ * @return the directory the segments live in
+ */
+ public static Path directoryIn(final Path dataDirectory) {
+ return dataDirectory.resolve(DIRECTORY_NAME);
+ }
+
+ /**
+ * The segment file a height belongs to.
+ *
+ * @param directory the journal directory
+ * @param blockNumber the height
+ * @return the segment path
+ */
+ public static Path segmentFor(final Path directory, final long blockNumber) {
+ return directory.resolve("w-" + (blockNumber / SEGMENT_BLOCKS) + ".rlp");
+ }
+
+ /**
+ * Encode one frame payload.
+ *
+ * @param chainId the chain id
+ * @param heard the heard block
+ * @return the RLP payload
+ */
+ public static byte[] encode(final long chainId, final Heard heard) {
+ final BytesValueRLPOutput out = new BytesValueRLPOutput();
+ out.startList();
+ out.writeBytes(DOMAIN_BYTES);
+ out.writeLongScalar(chainId);
+ out.writeLongScalar(heard.blockNumber());
+ out.writeBytes(heard.blockHash());
+ PqAnchor.writeCertificate(out, heard.seals());
+ out.endList();
+ return out.encoded().toArrayUnsafe();
+ }
+
+ /**
+ * Append one heard block to its segment. Best effort, unsynced.
+ *
+ * @param directory the journal directory
+ * @param chainId the chain id
+ * @param heard the heard block
+ * @throws IOException when the file system refuses; the caller logs and carries on
+ */
+ public static void append(final Path directory, final long chainId, final Heard heard)
+ throws IOException {
+ Files.createDirectories(directory);
+ final byte[] payload = encode(chainId, heard);
+ final CRC32 crc = new CRC32();
+ crc.update(payload, 0, payload.length);
+ final ByteBuffer frame = ByteBuffer.allocate(4 + payload.length + 4);
+ frame.putInt(payload.length);
+ frame.put(payload);
+ frame.putInt((int) crc.getValue());
+ frame.flip();
+ try (FileChannel channel =
+ FileChannel.open(
+ segmentFor(directory, heard.blockNumber()),
+ StandardOpenOption.CREATE,
+ StandardOpenOption.WRITE,
+ StandardOpenOption.APPEND)) {
+ while (frame.hasRemaining()) {
+ channel.write(frame);
+ }
+ }
+ }
+
+ /**
+ * Delete every segment that lies WHOLLY below the window.
+ *
+ * @param directory the journal directory
+ * @param highestBlock the highest height heard
+ * @param window the retention window in blocks
+ * @return how many segment files were deleted
+ */
+ public static int pruneSegments(final Path directory, final long highestBlock, final long window) {
+ if (directory == null || !Files.isDirectory(directory)) {
+ return 0;
+ }
+ final long oldestKept = Math.max(0L, highestBlock - window);
+ int deleted = 0;
+ try (DirectoryStream stream = Files.newDirectoryStream(directory)) {
+ for (final Path file : stream) {
+ final Matcher m = SEGMENT_NAME.matcher(file.getFileName().toString());
+ if (!m.matches()) {
+ continue;
+ }
+ final long lastHeightOfSegment = (Long.parseLong(m.group(1)) + 1L) * SEGMENT_BLOCKS - 1L;
+ if (lastHeightOfSegment < oldestKept) {
+ try {
+ Files.deleteIfExists(file);
+ deleted++;
+ } catch (final IOException e) {
+ LOG.debug("AERE FEREASTRA-SIGILII: could not delete {} ({}).", file, e.toString());
+ }
+ }
+ }
+ } catch (final IOException | RuntimeException e) {
+ LOG.debug("AERE FEREASTRA-SIGILII: could not list {} ({}).", directory, e.toString());
+ }
+ return deleted;
+ }
+
+ /**
+ * Read every frame for a height range, oldest first. Never throws; never verifies.
+ *
+ * @param directory the journal directory
+ * @param chainId the chain id this node runs
+ * @param fromBlock the lowest height wanted, inclusive
+ * @param toBlock the highest height wanted, inclusive
+ * @return the frames that decoded, for this chain, within the range
+ */
+ public static List readAll(
+ final Path directory, final long chainId, final long fromBlock, final long toBlock) {
+ final List out = new ArrayList<>();
+ if (directory == null || toBlock < fromBlock || !Files.isDirectory(directory)) {
+ return out;
+ }
+ final List segments = new ArrayList<>();
+ try (DirectoryStream stream = Files.newDirectoryStream(directory)) {
+ for (final Path file : stream) {
+ final Matcher m = SEGMENT_NAME.matcher(file.getFileName().toString());
+ if (!m.matches()) {
+ continue;
+ }
+ final long first = Long.parseLong(m.group(1)) * SEGMENT_BLOCKS;
+ final long last = first + SEGMENT_BLOCKS - 1L;
+ if (last >= fromBlock && first <= toBlock) {
+ segments.add(file);
+ }
+ }
+ } catch (final IOException | RuntimeException e) {
+ LOG.warn(
+ "AERE FEREASTRA-SIGILII: the window journal {} could not be listed ({}); IGNORED, the "
+ + "window starts empty exactly as it did before this journal existed.",
+ directory,
+ e.toString());
+ return out;
+ }
+ for (final Path segment : segments) {
+ readSegment(segment, chainId, fromBlock, toBlock, out);
+ }
+ out.sort(Comparator.comparingLong(Heard::blockNumber));
+ return out;
+ }
+
+ private static void readSegment(
+ final Path segment,
+ final long chainId,
+ final long fromBlock,
+ final long toBlock,
+ final List out) {
+ final byte[] raw;
+ try {
+ if (!Files.isRegularFile(segment)) {
+ return;
+ }
+ final long size = Files.size(segment);
+ if (size <= 0 || size > MAX_SEGMENT_BYTES) {
+ LOG.warn(
+ "AERE FEREASTRA-SIGILII: segment {} has an implausible size ({} bytes); IGNORED.",
+ segment,
+ size);
+ return;
+ }
+ raw = Files.readAllBytes(segment);
+ } catch (final IOException | RuntimeException e) {
+ LOG.warn(
+ "AERE FEREASTRA-SIGILII: segment {} could not be read ({}); IGNORED.",
+ segment,
+ e.toString());
+ return;
+ }
+ final ByteBuffer buffer = ByteBuffer.wrap(raw);
+ int frames = 0;
+ while (buffer.remaining() >= 8) {
+ final int length = buffer.getInt();
+ if (length <= 0 || length > MAX_FRAME_BYTES || buffer.remaining() < length + 4) {
+ LOG.info(
+ "AERE FEREASTRA-SIGILII: segment {} ends in a torn or implausible frame after {} good "
+ + "frame(s); what follows it is dropped.",
+ segment,
+ frames);
+ return;
+ }
+ final byte[] payload = new byte[length];
+ buffer.get(payload);
+ final int storedCrc = buffer.getInt();
+ final CRC32 crc = new CRC32();
+ crc.update(payload, 0, payload.length);
+ if ((int) crc.getValue() != storedCrc) {
+ LOG.warn(
+ "AERE FEREASTRA-SIGILII: segment {} holds a frame whose checksum does not match after "
+ + "{} good frame(s); it and what follows it are dropped.",
+ segment,
+ frames);
+ return;
+ }
+ frames++;
+ final Heard heard = decode(payload, chainId);
+ if (heard != null && heard.blockNumber() >= fromBlock && heard.blockNumber() <= toBlock) {
+ out.add(heard);
+ }
+ }
+ }
+
+ private static Heard decode(final byte[] payload, final long chainId) {
+ try {
+ final RLPInput in = new BytesValueRLPInput(Bytes.wrap(payload), false);
+ in.enterList();
+ if (!DOMAIN_BYTES.equals(in.readBytes())) {
+ return null;
+ }
+ if (in.readLongScalar() != chainId) {
+ return null;
+ }
+ final long blockNumber = in.readLongScalar();
+ final Bytes blockHash = in.readBytes();
+ if (blockNumber < 0 || blockHash.size() != 32) {
+ return null;
+ }
+ final List seals = new ArrayList<>();
+ in.enterList();
+ while (!in.isEndOfCurrentList()) {
+ if (seals.size() >= MAX_SEALS) {
+ return null;
+ }
+ in.enterList();
+ final int index = in.readIntScalar();
+ final Bytes signature = in.readBytes();
+ in.leaveList();
+ seals.add(new FalconSeal(index, signature));
+ }
+ in.leaveList();
+ in.leaveList();
+ return new Heard(blockNumber, blockHash, seals);
+ } catch (final RuntimeException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Count the frames of a journal, for tests and for the operator's curiosity. Never throws.
+ *
+ * @param directory the journal directory
+ * @param chainId the chain id
+ * @return how many frames decode for this chain, at any height
+ */
+ public static int frameCount(final Path directory, final long chainId) {
+ return readAll(directory, chainId, 0L, Long.MAX_VALUE).size();
+ }
+
+ /**
+ * The seals of one frame collection, for callers that hold a plain collection.
+ *
+ * @param blockNumber the height
+ * @param blockHash the on-chain hash
+ * @param seals the heard seals
+ * @return the frame value, seals index-sorted
+ */
+ public static Heard heard(
+ final long blockNumber, final Bytes blockHash, final Collection seals) {
+ return new Heard(blockNumber, blockHash, PqAnchor.sortedByIndex(new ArrayList<>(seals)));
+ }
+}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreator.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreator.java
new file mode 100644
index 0000000..e43afac
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreator.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/aere-bft-prevrandao-block-creation.patch.
+ */
+package org.hyperledger.besu.consensus.common.bft.blockcreation;
+
+import org.hyperledger.besu.config.BftConfigOptions;
+import org.hyperledger.besu.consensus.common.ForksSchedule;
+import org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions;
+import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec;
+import org.hyperledger.besu.consensus.common.bft.BftHelpers;
+import org.hyperledger.besu.consensus.common.bft.BftProtocolSchedule;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.ethereum.ProtocolContext;
+import org.hyperledger.besu.ethereum.blockcreation.AbstractBlockCreator;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder;
+import org.hyperledger.besu.ethereum.core.MiningConfiguration;
+import org.hyperledger.besu.ethereum.core.SealableBlockHeader;
+import org.hyperledger.besu.ethereum.eth.manager.EthScheduler;
+import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
+import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
+import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import org.apache.tuweni.bytes.Bytes32;
+
+/** The Bft block creator. */
+// This class is responsible for creating a block without committer seals (basically it was just
+// too hard to coordinate with the state machine).
+public class BftBlockCreator extends AbstractBlockCreator {
+
+ private final BftExtraDataCodec bftExtraDataCodec;
+
+ /**
+ * Instantiates a new Bft block creator.
+ *
+ * @param miningConfiguration the mining parameters
+ * @param forksSchedule the forks schedule
+ * @param localAddress the local address
+ * @param extraDataCalculator the extra data calculator
+ * @param transactionPool the pending transactions
+ * @param protocolContext the protocol context
+ * @param protocolSchedule the protocol schedule
+ * @param bftExtraDataCodec the bft extra data codec
+ * @param ethScheduler the scheduler for asynchronous block creation tasks
+ */
+ public BftBlockCreator(
+ final MiningConfiguration miningConfiguration,
+ final ForksSchedule extends BftConfigOptions> forksSchedule,
+ final Address localAddress,
+ final ExtraDataCalculator extraDataCalculator,
+ final TransactionPool transactionPool,
+ final ProtocolContext protocolContext,
+ final ProtocolSchedule protocolSchedule,
+ final BftExtraDataCodec bftExtraDataCodec,
+ final EthScheduler ethScheduler) {
+ super(
+ miningConfiguration.setCoinbase(localAddress),
+ miningBeneficiaryCalculator(protocolSchedule),
+ extraDataCalculator,
+ transactionPool,
+ protocolContext,
+ protocolSchedule,
+ ethScheduler);
+ this.bftExtraDataCodec = bftExtraDataCodec;
+ }
+
+ @Override
+ public BlockCreationResult createBlock(final long timestamp, final BlockHeader parentHeader) {
+ ProtocolSpec protocolSpec =
+ ((BftProtocolSchedule) protocolSchedule)
+ .getByBlockNumberOrTimestamp(parentHeader.getNumber() + 1, timestamp);
+
+ if (protocolSpec.getWithdrawalsProcessor().isPresent()) {
+ return createEmptyWithdrawalsBlock(timestamp, parentHeader);
+ } else {
+ return createBlock(
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ Optional.of(Bytes32.wrap(BftHelpers.EXPECTED_MIX_HASH.getBytes())),
+ Optional.empty(),
+ Optional.empty(),
+ timestamp,
+ true,
+ parentHeader);
+ }
+ }
+
+ /**
+ * Construct a mining beneficiary calculator using the given protocolSchedule
+ *
+ * @param protocolSchedule protocol schedule
+ * @return mining beneficiary calculator
+ */
+ public static MiningBeneficiaryCalculator miningBeneficiaryCalculator(
+ final ProtocolSchedule protocolSchedule) {
+ return (blockTimestamp, pendingHeader) -> {
+ BlockHeader newBlockHeader =
+ BlockHeaderBuilder.createDefault()
+ .coinbase(pendingHeader.getCoinbase())
+ .buildBlockHeader();
+ ProtocolSpec protocolSpec =
+ ((BftProtocolSchedule) protocolSchedule)
+ .getByBlockNumberOrTimestamp(pendingHeader.getNumber(), blockTimestamp);
+ return protocolSpec.getMiningBeneficiaryCalculator().calculateBeneficiary(newBlockHeader);
+ };
+ }
+
+ @Override
+ public BlockCreationResult createEmptyWithdrawalsBlock(
+ final long timestamp, final BlockHeader parentHeader) {
+ return createBlock(
+ Optional.empty(),
+ Optional.empty(),
+ Optional.of(Collections.emptyList()),
+ Optional.of(Bytes32.wrap(BftHelpers.EXPECTED_MIX_HASH.getBytes())),
+ Optional.empty(),
+ Optional.empty(),
+ timestamp,
+ true,
+ parentHeader);
+ }
+
+ @Override
+ protected BlockHeader createFinalBlockHeader(final SealableBlockHeader sealableBlockHeader) {
+ final BlockHeaderBuilder builder =
+ BlockHeaderBuilder.create()
+ .populateFrom(sealableBlockHeader)
+ .mixHash(BftHelpers.EXPECTED_MIX_HASH)
+ .nonce(0L)
+ .blockHeaderFunctions(BftBlockHeaderFunctions.forCommittedSeal(bftExtraDataCodec));
+
+ return builder.buildBlockHeader();
+ }
+}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftMiningCoordinator.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftMiningCoordinator.java
new file mode 100644
index 0000000..da4eeec
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftMiningCoordinator.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/aere-bft-coordinator-lifecycle-sync.patch.
+ */
+package org.hyperledger.besu.consensus.common.bft.blockcreation;
+
+import org.hyperledger.besu.consensus.common.bft.BftEventQueue;
+import org.hyperledger.besu.consensus.common.bft.BftExecutors;
+import org.hyperledger.besu.consensus.common.bft.BftProcessor;
+import org.hyperledger.besu.consensus.common.bft.events.NewChainHead;
+import org.hyperledger.besu.consensus.common.bft.statemachine.BftEventHandler;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.datatypes.Wei;
+import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
+import org.hyperledger.besu.ethereum.chain.BlockAddedEvent;
+import org.hyperledger.besu.ethereum.chain.BlockAddedObserver;
+import org.hyperledger.besu.ethereum.chain.Blockchain;
+import org.hyperledger.besu.ethereum.core.Block;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+import org.hyperledger.besu.ethereum.core.Transaction;
+import org.hyperledger.besu.ethereum.eth.sync.state.SyncState;
+import org.hyperledger.besu.plugin.services.BesuEvents;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** The Bft mining coordinator. */
+public class BftMiningCoordinator implements MiningCoordinator, BlockAddedObserver {
+
+ private enum State {
+ /** Idle state. */
+ IDLE,
+ /** Running state. */
+ RUNNING,
+ /** Stopped state. */
+ STOPPED,
+ /** Paused state. */
+ PAUSED,
+ }
+
+ private static final Logger LOG = LoggerFactory.getLogger(BftMiningCoordinator.class);
+
+ private final BftEventHandler eventHandler;
+ private final BftProcessor bftProcessor;
+ private final BftBlockCreatorFactory> blockCreatorFactory;
+
+ /** The Blockchain. */
+ protected final Blockchain blockchain;
+
+ private final BftEventQueue eventQueue;
+ private final BftExecutors bftExecutors;
+
+ private long blockAddedObserverId;
+ private final AtomicReference state = new AtomicReference<>(State.PAUSED);
+
+ private SyncState syncState;
+
+ /**
+ * Instantiates a new Bft mining coordinator.
+ *
+ * @param bftExecutors the bft executors
+ * @param eventHandler the event handler
+ * @param bftProcessor the bft processor
+ * @param blockCreatorFactory the block creator factory
+ * @param blockchain the blockchain
+ * @param eventQueue the event queue
+ */
+ public BftMiningCoordinator(
+ final BftExecutors bftExecutors,
+ final BftEventHandler eventHandler,
+ final BftProcessor bftProcessor,
+ final BftBlockCreatorFactory> blockCreatorFactory,
+ final Blockchain blockchain,
+ final BftEventQueue eventQueue) {
+ this.bftExecutors = bftExecutors;
+ this.eventHandler = eventHandler;
+ this.bftProcessor = bftProcessor;
+ this.blockCreatorFactory = blockCreatorFactory;
+ this.eventQueue = eventQueue;
+
+ this.blockchain = blockchain;
+ }
+
+ /**
+ * Instantiates a new Bft mining coordinator.
+ *
+ * @param bftExecutors the bft executors
+ * @param eventHandler the event handler
+ * @param bftProcessor the bft processor
+ * @param blockCreatorFactory the block creator factory
+ * @param blockchain the blockchain
+ * @param eventQueue the event queue
+ * @param syncState the sync state
+ */
+ public BftMiningCoordinator(
+ final BftExecutors bftExecutors,
+ final BftEventHandler eventHandler,
+ final BftProcessor bftProcessor,
+ final BftBlockCreatorFactory> blockCreatorFactory,
+ final Blockchain blockchain,
+ final BftEventQueue eventQueue,
+ final SyncState syncState) {
+ this.bftExecutors = bftExecutors;
+ this.eventHandler = eventHandler;
+ this.bftProcessor = bftProcessor;
+ this.blockCreatorFactory = blockCreatorFactory;
+ this.eventQueue = eventQueue;
+
+ this.blockchain = blockchain;
+ this.syncState = syncState;
+ }
+
+ @Override
+ public void start() {
+ if (state.compareAndSet(State.IDLE, State.RUNNING)
+ || state.compareAndSet(State.STOPPED, State.RUNNING)) {
+ bftProcessor.start();
+ bftExecutors.start();
+ blockAddedObserverId = blockchain.observeBlockAdded(this);
+ eventHandler.start();
+ bftExecutors.executeBftProcessor(bftProcessor);
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (state.compareAndSet(State.RUNNING, State.STOPPED)) {
+ blockchain.removeObserver(blockAddedObserverId);
+ bftProcessor.stop();
+ // Make sure the processor has stopped before shutting down the executors
+ try {
+ bftProcessor.awaitStop();
+ } catch (final InterruptedException e) {
+ LOG.debug("Interrupted while waiting for BftProcessor to stop.", e);
+ Thread.currentThread().interrupt();
+ }
+ eventHandler.stop();
+ bftExecutors.stop();
+ }
+ }
+
+ @Override
+ public void subscribe() {
+ if (syncState == null) {
+ return;
+ }
+ syncState.subscribeSyncStatus(
+ syncStatus -> {
+ if (syncState.syncTarget().isPresent() || !syncState.isInitialSyncPhaseDone()) {
+ // We're syncing so stop doing other stuff
+ LOG.info("Stopping BFT mining coordinator while we are syncing");
+ stop();
+ } else {
+ LOG.info("Starting BFT mining coordinator following sync");
+ enable();
+ start();
+ }
+ });
+
+ syncState.subscribeCompletionReached(
+ new BesuEvents.InitialSyncCompletionListener() {
+ @Override
+ public void onInitialSyncCompleted() {
+ LOG.info("Starting BFT mining coordinator following initial sync");
+ enable();
+ start();
+ }
+
+ @Override
+ public void onInitialSyncRestart() {
+ // Nothing to do. The mining coordinator won't be started until
+ // sync has completed.
+ }
+ });
+ }
+
+ @Override
+ public void awaitStop() throws InterruptedException {
+ bftExecutors.awaitStop();
+ }
+
+ @Override
+ public synchronized boolean enable() {
+ // AERE D-434 (81a600236a, issue #10877): synchronized with start()/stop() so the multi-step
+ // state checks below cannot interleave with a concurrent lifecycle transition.
+ // Return true if we're already running or idle, or successfully switch to idle
+ if (state.get() == State.RUNNING
+ || state.get() == State.IDLE
+ || state.compareAndSet(State.PAUSED, State.IDLE)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public synchronized boolean disable() {
+ // AERE D-434 (81a600236a, issue #10877): synchronized with start()/stop() so the multi-step
+ // state checks below cannot interleave with a concurrent lifecycle transition.
+ if (state.get() == State.PAUSED
+ || state.compareAndSet(State.IDLE, State.PAUSED)
+ || state.compareAndSet(State.RUNNING, State.PAUSED)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isMining() {
+ return state.get() == State.RUNNING;
+ }
+
+ @Override
+ public Wei getMinTransactionGasPrice() {
+ return blockCreatorFactory.getMinTransactionGasPrice();
+ }
+
+ @Override
+ public Wei getMinPriorityFeePerGas() {
+ return blockCreatorFactory.getMinPriorityFeePerGas();
+ }
+
+ @Override
+ public Optional getCoinbase() {
+ return Optional.of(blockCreatorFactory.getLocalAddress());
+ }
+
+ @Override
+ public Optional createBlock(
+ final BlockHeader parentHeader,
+ final List transactions,
+ final List ommers) {
+ // One-off block creation has not been implemented
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional createBlock(final BlockHeader parentHeader, final long timestamp) {
+ // One-off block creation has not been implemented
+ return Optional.empty();
+ }
+
+ @Override
+ public void changeTargetGasLimit(final Long targetGasLimit) {
+ blockCreatorFactory.changeTargetGasLimit(targetGasLimit);
+ }
+
+ @Override
+ public void onBlockAdded(final BlockAddedEvent event) {
+ if (event.isNewCanonicalHead()) {
+ LOG.trace("New canonical head detected");
+ eventQueue.add(new NewChainHead(event.getHeader()));
+ }
+ }
+
+ @Override
+ public void removeObserver() {
+ blockchain.removeObserver(blockAddedObserverId);
+ }
+}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/messagewrappers/BftMessage.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/messagewrappers/BftMessage.java
new file mode 100644
index 0000000..e52c529
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/messagewrappers/BftMessage.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/aere-bft-bounded-message-lists.patch.
+ */
+package org.hyperledger.besu.consensus.common.bft.messagewrappers;
+
+import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
+import org.hyperledger.besu.consensus.common.bft.payload.Authored;
+import org.hyperledger.besu.consensus.common.bft.payload.Payload;
+import org.hyperledger.besu.consensus.common.bft.payload.RoundSpecific;
+import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
+import org.hyperledger.besu.crypto.SECPSignature;
+import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
+import org.hyperledger.besu.ethereum.rlp.RLPInput;
+
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.function.Function;
+
+import org.apache.tuweni.bytes.Bytes;
+
+/**
+ * The Bft message.
+ *
+ * @param
the type of Payload
+ */
+public class BftMessage
implements Authored, RoundSpecific {
+
+ /**
+ * Maximum number of entries permitted in any embedded list (round-changes, prepares) when
+ * decoding a BFT message. AERE D-434: ported from upstream hyperledger/besu dd4101c9ec.
+ */
+ public static final int MAX_LIST_ENTRIES = 512;
+
+ private final SignedData
payload;
+
+ /**
+ * Instantiates a new Bft message.
+ *
+ * @param payload the payload
+ */
+ public BftMessage(final SignedData
payload) {
+ this.payload = payload;
+ }
+
+ @Override
+ public Address getAuthor() {
+ return payload.getAuthor();
+ }
+
+ @Override
+ public ConsensusRoundIdentifier getRoundIdentifier() {
+ return payload.getPayload().getRoundIdentifier();
+ }
+
+ /**
+ * Encode.
+ *
+ * @return the bytes
+ */
+ public Bytes encode() {
+ final BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
+ payload.writeTo(rlpOut);
+ return rlpOut.encoded();
+ }
+
+ /**
+ * Gets signed payload.
+ *
+ * @return the signed payload
+ */
+ public SignedData
getSignedPayload() {
+ return payload;
+ }
+
+ /**
+ * Gets message type.
+ *
+ * @return the message type
+ */
+ public int getMessageType() {
+ return payload.getPayload().getMessageType();
+ }
+
+ /**
+ * Gets payload.
+ *
+ * @return the payload
+ */
+ protected P getPayload() {
+ return payload.getPayload();
+ }
+
+ /**
+ * Read payload.
+ *
+ * @param the type parameter of Payload
+ * @param rlpInput the rlp input
+ * @param decoder the decoder
+ * @return the signed data
+ */
+ protected static SignedData readPayload(
+ final RLPInput rlpInput, final Function decoder) {
+ rlpInput.enterList();
+ final T unsignedMessageData = decoder.apply(rlpInput);
+ final SECPSignature signature =
+ rlpInput.readBytes((SignatureAlgorithmFactory.getInstance()::decodeSignature));
+ rlpInput.leaveList();
+
+ return SignedData.create(unsignedMessageData, signature);
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", BftMessage.class.getSimpleName() + "[", "]")
+ .add("payload=" + payload)
+ .toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final BftMessage> that = (BftMessage>) o;
+ return Objects.equals(payload, that.payload);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(payload);
+ }
+}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/network/ValidatorPeers.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/network/ValidatorPeers.java
new file mode 100644
index 0000000..649fa75
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/network/ValidatorPeers.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
+ */
+package org.hyperledger.besu.consensus.common.bft.network;
+
+import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
+import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection;
+import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection.PeerNotConnected;
+import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Responsible for tracking the network peers which have a connection to this node, then
+ * multicasting packets to ONLY the peers which have been identified as being validators.
+ */
+public class ValidatorPeers implements ValidatorMulticaster, PeerConnectionTracker {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ValidatorPeers.class);
+
+ // It's possible for multiple connections between peers to exist for brief periods, so map each
+ // address to a set of connections
+ private final Map> connectionsByAddress = new ConcurrentHashMap<>();
+ private final ValidatorProvider validatorProvider;
+ private final String protocolName;
+
+ /**
+ * Instantiates a new Validator peers.
+ *
+ * @param validatorProvider the validator provider
+ * @param protocolName the protocol name
+ */
+ public ValidatorPeers(final ValidatorProvider validatorProvider, final String protocolName) {
+ this.validatorProvider = validatorProvider;
+ this.protocolName = protocolName;
+ }
+
+ @Override
+ public void add(final PeerConnection newConnection) {
+ final Address peerAddress = newConnection.getPeerInfo().getAddress();
+ final Set connections =
+ connectionsByAddress.computeIfAbsent(
+ peerAddress, (k) -> Collections.newSetFromMap(new ConcurrentHashMap<>()));
+ connections.add(newConnection);
+ }
+
+ @Override
+ public void remove(final PeerConnection removedConnection) {
+ final Address peerAddress = removedConnection.getPeerInfo().getAddress();
+ final Set connections = connectionsByAddress.get(peerAddress);
+ if (connections == null) {
+ return;
+ }
+ connections.remove(removedConnection);
+ }
+
+ @Override
+ public void send(final MessageData message) {
+ sendMessageToSpecificAddresses(getLatestValidators(), message);
+ }
+
+ @Override
+ public void send(final MessageData message, final Collection denylist) {
+ final Collection includedValidators =
+ getLatestValidators().stream()
+ .filter(a -> !denylist.contains(a))
+ .collect(Collectors.toSet());
+ sendMessageToSpecificAddresses(includedValidators, message);
+ }
+
+ private void sendMessageToSpecificAddresses(
+ final Collection recipients, final MessageData message) {
+ LOG.trace(
+ "Sending message to peers messageCode={} recipients={}", message.getCode(), recipients);
+ // AERE AIP-22 stage 3: from H_id the recipients are ids; they are reached through the node-key
+ // address their v3 row lists as transport, or through every peer when a row lists none
+ final java.util.Optional> targets =
+ AerePqIdentity.transportTargets(recipients);
+ (targets.isPresent()
+ ? targets.get().stream().map(connectionsByAddress::get).filter(Objects::nonNull)
+ : connectionsByAddress.values().stream())
+ .flatMap(Set::stream)
+ .distinct()
+ .forEach(
+ connection -> {
+ try {
+ connection.sendForProtocol(protocolName, message);
+ } catch (final PeerNotConnected peerNotConnected) {
+ LOG.trace(
+ "Lost connection to a validator. remoteAddress={} peerInfo={}",
+ connection.getRemoteAddress(),
+ connection.getPeerInfo());
+ }
+ });
+ }
+
+ private Collection getLatestValidators() {
+ return validatorProvider.getValidatorsAtHead();
+ }
+}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/payload/SignedData.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/payload/SignedData.java
new file mode 100644
index 0000000..2c59e69
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/payload/SignedData.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
+ */
+package org.hyperledger.besu.consensus.common.bft.payload;
+
+import org.hyperledger.besu.consensus.common.bft.AerePqFinality;
+import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
+import org.hyperledger.besu.crypto.SECPSignature;
+import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.datatypes.Hash;
+import org.hyperledger.besu.ethereum.core.Util;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
+import org.hyperledger.besu.ethereum.rlp.RLPOutput;
+
+import java.math.BigInteger;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+import org.apache.tuweni.bytes.Bytes;
+
+/**
+ * The type Signed data.
+ *
+ * @param the type parameter of Payload
+ */
+public class SignedData implements Authored {
+
+ private static final BigInteger HALF_CURVE_ORDER =
+ SignatureAlgorithmFactory.getInstance().getHalfCurveOrder();
+
+ private final Address sender;
+ private final SECPSignature signature;
+ private final M unsignedPayload;
+
+ /**
+ * Create signed data.
+ *
+ * @param the type parameter
+ * @param payload the payload
+ * @param signature the signature
+ * @return the signed data
+ * @throws IllegalArgumentException if the signature is invalid
+ */
+ public static SignedData create(
+ final T payload, final SECPSignature signature) {
+ final Hash msgHash = payload.hashForSignature();
+ // AERE AIP-22 stage 3: from H_id the author is the post-quantum id of the signer's v3 row
+ // AERE AIP-22 stage 4: from H_fin the author is the id of the row the message's post-quantum seal index names, and the
+ // envelope's ECDSA signature is not read for authorship (a message without a post-quantum seal has no author then)
+ final long aereHeight = payload.getRoundIdentifier().getSequenceNumber();
+ return new SignedData<>(
+ payload,
+ AerePqFinality.active(aereHeight)
+ ? AerePqFinality.authorFromSeal(payload, aereHeight)
+ : AerePqIdentity.author(Util.signatureToAddress(signature, msgHash), aereHeight),
+ signature);
+ }
+
+ private SignedData(final M unsignedPayload, final Address sender, final SECPSignature signature) {
+ if (!(signature.getS().compareTo(HALF_CURVE_ORDER) <= 0)) {
+ throw new IllegalArgumentException("Signature is invalid");
+ }
+ this.unsignedPayload = unsignedPayload;
+ this.sender = sender;
+ this.signature = signature;
+ }
+
+ @Override
+ public Address getAuthor() {
+ return sender;
+ }
+
+ /**
+ * Gets payload.
+ *
+ * @return the payload
+ */
+ public M getPayload() {
+ return unsignedPayload;
+ }
+
+ /**
+ * Write to.
+ *
+ * @param output the output
+ */
+ public void writeTo(final RLPOutput output) {
+ output.startList();
+ unsignedPayload.writeTo(output);
+ output.writeBytes(signature.encodedBytes());
+ output.endList();
+ }
+
+ /**
+ * Encode.
+ *
+ * @return the bytes
+ */
+ public Bytes encode() {
+ final BytesValueRLPOutput rlpEncode = new BytesValueRLPOutput();
+ writeTo(rlpEncode);
+ return rlpEncode.encoded();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final SignedData> that = (SignedData>) o;
+ return Objects.equals(sender, that.sender)
+ && Objects.equals(signature, that.signature)
+ && Objects.equals(unsignedPayload, that.unsignedPayload);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(sender, signature, unsignedPayload);
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", SignedData.class.getSimpleName() + "[", "]")
+ .add("sender=" + sender)
+ .add("signature=" + signature)
+ .add("unsignedPayload=" + unsignedPayload)
+ .toString();
+ }
+}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTally.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTally.java
new file mode 100644
index 0000000..fed8a2a
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTally.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
+ */
+package org.hyperledger.besu.consensus.common.validator.blockbased;
+
+import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
+import org.hyperledger.besu.consensus.common.validator.ValidatorVote;
+import org.hyperledger.besu.datatypes.Address;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+
+import com.google.common.collect.Maps;
+
+/** Tracks the current list of validators and votes to add or drop validators. */
+class VoteTally {
+
+ private final NavigableSet currentValidators;
+
+ private final Map> addVotesBySubject;
+ private final Map> removeVotesBySubject;
+
+ VoteTally(final Collection initialValidators) {
+ this(new TreeSet<>(initialValidators), new HashMap<>(), new HashMap<>());
+ }
+
+ private VoteTally(
+ final Collection initialValidators,
+ final Map> addVotesBySubject,
+ final Map> removeVotesBySubject) {
+ this.currentValidators = new TreeSet<>(initialValidators);
+ this.addVotesBySubject = addVotesBySubject;
+ this.removeVotesBySubject = removeVotesBySubject;
+ }
+
+ /**
+ * Add a vote to the current tally. The current validator list will be updated if this vote takes
+ * the tally past the required votes to approve the change.
+ *
+ * @param validatorVote The vote which was cast in a block header.
+ */
+ void addVote(final ValidatorVote validatorVote) {
+ final Set addVotesForSubject =
+ addVotesBySubject.computeIfAbsent(validatorVote.getRecipient(), target -> new HashSet<>());
+ final Set removeVotesForSubject =
+ removeVotesBySubject.computeIfAbsent(
+ validatorVote.getRecipient(), target -> new HashSet<>());
+
+ if (validatorVote.isAuthVote()) {
+ addVotesForSubject.add(validatorVote.getProposer());
+ removeVotesForSubject.remove(validatorVote.getProposer());
+ } else {
+ removeVotesForSubject.add(validatorVote.getProposer());
+ addVotesForSubject.remove(validatorVote.getProposer());
+ }
+
+ final int validatorLimit = validatorLimit();
+ if (addVotesForSubject.size() >= validatorLimit) {
+ currentValidators.add(validatorVote.getRecipient());
+ discardOutstandingVotesFor(validatorVote.getRecipient());
+ }
+ if (removeVotesForSubject.size() >= validatorLimit) {
+ currentValidators.remove(validatorVote.getRecipient());
+ discardOutstandingVotesFor(validatorVote.getRecipient());
+ addVotesBySubject.values().forEach(votes -> votes.remove(validatorVote.getRecipient()));
+ removeVotesBySubject.values().forEach(votes -> votes.remove(validatorVote.getRecipient()));
+ }
+ }
+
+ private void discardOutstandingVotesFor(final Address subject) {
+ addVotesBySubject.remove(subject);
+ removeVotesBySubject.remove(subject);
+ }
+
+ Set getOutstandingAddVotesFor(final Address subject) {
+ return Optional.ofNullable(addVotesBySubject.get(subject)).orElse(Collections.emptySet());
+ }
+
+ Set getOutstandingRemoveVotesFor(final Address subject) {
+ return Optional.ofNullable(removeVotesBySubject.get(subject)).orElse(Collections.emptySet());
+ }
+
+ private int validatorLimit() {
+ return (currentValidators.size() / 2) + 1;
+ }
+
+ /**
+ * Reset the outstanding vote tallies as required at each epoch. The current validator list is
+ * unaffected.
+ */
+ void discardOutstandingVotes() {
+ addVotesBySubject.clear();
+ removeVotesBySubject.clear();
+ }
+
+ /**
+ * AERE AIP-22 stage 3: after block H_id - 1 the validators become the sorted post-quantum ids of the
+ * same members, and votes still open on classical addresses are discarded (they name addresses the
+ * id space no longer contains). Anywhere else, and when the identity switch is not armed, nothing
+ * changes.
+ *
+ * @param blockNumber the block whose effects this tally now includes
+ */
+ void translateToIds(final long blockNumber) {
+ final Collection translated =
+ AerePqIdentity.translateSetAfter(currentValidators, blockNumber);
+ if (translated != currentValidators) {
+ currentValidators.clear();
+ currentValidators.addAll(translated);
+ discardOutstandingVotes();
+ }
+ }
+
+ /**
+ * The validator addresses
+ *
+ * @return The collection of validators after the voting at the most recent block has been
+ * finalised.
+ */
+ Collection getValidators() {
+ return currentValidators;
+ }
+
+ VoteTally copy() {
+ final Map> addVotesBySubject = Maps.newHashMap();
+ final Map> removeVotesBySubject = Maps.newHashMap();
+
+ this.addVotesBySubject.forEach(
+ (key, value) -> addVotesBySubject.put(key, new TreeSet<>(value)));
+ this.removeVotesBySubject.forEach(
+ (key, value) -> removeVotesBySubject.put(key, new TreeSet<>(value)));
+
+ return new VoteTally(
+ new TreeSet<>(this.currentValidators), addVotesBySubject, removeVotesBySubject);
+ }
+}
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTallyUpdater.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTallyUpdater.java
new file mode 100644
index 0000000..0da2c63
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTallyUpdater.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
+ */
+package org.hyperledger.besu.consensus.common.validator.blockbased;
+
+import org.hyperledger.besu.consensus.common.BlockInterface;
+import org.hyperledger.besu.consensus.common.EpochManager;
+import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
+import org.hyperledger.besu.consensus.common.validator.ValidatorVote;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.ethereum.chain.Blockchain;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+
+import java.util.Collection;
+import java.util.Optional;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides the logic to extract vote tally state from the blockchain and update it as blocks are
+ * added.
+ */
+class VoteTallyUpdater {
+
+ private static final Logger LOG = LoggerFactory.getLogger(VoteTallyUpdater.class);
+
+ private final EpochManager epochManager;
+ private final BlockInterface blockInterface;
+
+ VoteTallyUpdater(final EpochManager epochManager, final BlockInterface blockInterface) {
+ this.epochManager = epochManager;
+ this.blockInterface = blockInterface;
+ }
+
+ /**
+ * Create a new VoteTally based on the current blockchain state.
+ *
+ * @param blockchain the blockchain to load the current state from
+ * @return a VoteTally reflecting the state of the blockchain head
+ */
+ VoteTally buildVoteTallyFromBlockchain(final Blockchain blockchain) {
+ final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber();
+ final long epochBlockNumber = epochManager.getLastEpochBlock(chainHeadBlockNumber);
+ LOG.info("Loading validator voting state starting from block {}", epochBlockNumber);
+ final BlockHeader epochBlock = blockchain.getBlockHeader(epochBlockNumber).get();
+ final Collection initialValidators = blockInterface.validatorsInBlock(epochBlock);
+ final VoteTally voteTally = new VoteTally(initialValidators);
+ for (long blockNumber = epochBlockNumber + 1;
+ blockNumber <= chainHeadBlockNumber;
+ blockNumber++) {
+ updateForBlock(blockchain.getBlockHeader(blockNumber).get(), voteTally);
+ }
+ return voteTally;
+ }
+
+ /**
+ * Update the vote tally to reflect changes caused by appending a new block to the chain.
+ *
+ * @param header the header of the block being added
+ * @param voteTally the vote tally to update
+ */
+ void updateForBlock(final BlockHeader header, final VoteTally voteTally) {
+ if (epochManager.isEpochBlock(header.getNumber())) {
+ voteTally.discardOutstandingVotes();
+ } else {
+ final Optional vote = blockInterface.extractVoteFromHeader(header);
+ vote.ifPresent(voteTally::addVote);
+ }
+ // AERE AIP-22 stage 3: after block H_id - 1 the set becomes the post-quantum ids of its members;
+ // asked first, so that disarmed (or at any other block) the tally sees no extra call at all
+ if (AerePqIdentity.translatesAfter(header.getNumber())) {
+ voteTally.translateToIds(header.getNumber());
+ }
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowPersistenceTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowPersistenceTest.java
new file mode 100644
index 0000000..5c661d3
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowPersistenceTest.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright contributors to Besu / AERE Network.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.besu.consensus.common.bft;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
+import org.hyperledger.besu.crypto.SecureRandomProvider;
+import org.hyperledger.besu.datatypes.Hash;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
+
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.function.LongFunction;
+import java.util.stream.Stream;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.apache.tuweni.bytes.Bytes32;
+import org.bouncycastle.crypto.digests.KeccakDigest;
+import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
+import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * AIP-21, SECOND HALF (2026-09-18): the finality window survives a restart.
+ *
+ *
The rule of the house: no proof without its negative pair. Real Falcon-512 keys, the real
+ * genesis-anchored registry, the real commit message, a real directory. What each test pins down:
+ *
+ *
+ *
{@link #theWindowSurvivesARestartAndOnlyCanonicalBlocksComeBack()} - the point of the whole
+ * thing, with its negative: a journal frame for a hash that is NOT canonical at its height
+ * never comes back, and neither does a height this node's chain does not hold.
+ *
{@link #aForgedSealInTheJournalNeverEntersTheWindow()} - THE load-bearing assertion, the one
+ * the build-time negative control plants against: three forgeries beside two genuine seals.
+ *
{@link #aTornTailOrAFlippedByteCostsOnlyWhatFollowsIt()} - a node that died mid-append, a
+ * flipped byte, garbage, a directory where a file should be: never an exception, and the good
+ * frames BEFORE the damage still come back.
+ *
{@link #aRestoredWindowIsNotAppendedASecondTime()} - the journal does not grow by what it
+ * already holds every time the node restarts.
+ *
{@link #whatOneAppendCostsAgainstTheBlockInterval()} - the price on the consensus thread,
+ * as a number.
+ *
+ *
+ *
NOT MEASURED here: that a live validator answers {@code post-quantum} for a block heard before
+ * its restart. That is measured on testnet 28001 and recorded in the AIP.
+ */
+public class PqSealWindowPersistenceTest {
+
+ private static final long H = 1_000L;
+ private static final long ATTACH = 900L;
+ private static final int N = 7;
+ private static final long CHAIN_ID = 2_800L;
+ private static final long BLOCK_INTERVAL_MS = 523L;
+ private static final long HEAD = 5_000L;
+
+ @TempDir private Path tmp;
+
+ private Path dataDirectory;
+ private final List privateKeys = new ArrayList<>();
+ private final Map chain = new HashMap<>();
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ dataDirectory = Files.createDirectories(tmp.resolve("besu-data"));
+ final KeccakDigest kd = new KeccakDigest(256);
+ final StringBuilder manifest =
+ new StringBuilder("{\"config\":{\"aereFalconRegistry\":{")
+ .append(PqV2Fixture.manifestHeader(N, CHAIN_ID, H));
+ for (int i = 0; i < N; i++) {
+ privateKeys.add(PqV2Fixture.privateKey(i));
+ final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i);
+ kd.update(anchoredRow, 0, anchoredRow.length);
+ manifest.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, H));
+ }
+ final byte[] anchoredHash = new byte[32];
+ kd.doFinal(anchoredHash, 0);
+ manifest
+ .append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x")
+ .append("0".repeat(64))
+ .append("\":\"0x")
+ .append(Bytes.wrap(anchoredHash).toUnprefixedHexString())
+ .append("\"}}}}");
+ final Path genesisPath = tmp.resolve("genesis-registry.json");
+ Files.writeString(genesisPath, manifest.toString());
+ System.setProperty("aere.falcon.genesis", genesisPath.toAbsolutePath().toString());
+ System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
+ System.clearProperty(PqSealWindowStore.PROPERTY_PERSIST);
+
+ resetFalconSingleton();
+ bindTheRegistryScheduleTheWayALiveNodeDoes(genesisPath);
+ PqSealCache.instance().disablePersistence();
+ PqSealCache.instance().clear();
+ PqAnchorProducer.useConfigForTesting(
+ new PqAnchorConfig(CHAIN_ID, H, Map.of(H, 0, H + 10L, 3), OptionalInt.empty(), false));
+ chain.clear();
+ }
+
+ /**
+ * The HISTORY door ({@code verifyAtHistoric}) refuses to resolve a key above the arming height
+ * unless a registry schedule is loaded (D2, 2026-08-06): it will not fall back to the head
+ * registry. A live node holds that schedule from its genesis ({@code config.pqRegistryHash}); the
+ * window restore judges heights BELOW the head, so it goes through that door, and so this fixture
+ * binds the schedule exactly the way a node does. Measured 2026-09-18: without this every restore
+ * here returned zero, with the refusal in the log.
+ */
+ private void bindTheRegistryScheduleTheWayALiveNodeDoes(final Path genesisPath) throws Exception {
+ final PqRegistryHash.Registry held = PqRegistryHash.loadAuto(genesisPath);
+ String hash = PqRegistryHash.hashFor(held, CHAIN_ID);
+ if (!hash.startsWith("0x")) {
+ hash = "0x" + hash;
+ }
+ final Path schedule = tmp.resolve("genesis-schedule.json");
+ Files.writeString(
+ schedule,
+ "{\"config\":{\"pqRegistryHash\":[{\"block\":" + H + ",\"hash\":\"" + hash + "\"}]}}");
+ FalconSealSupport.instance()
+ .verifyRegistryBindingOrAbort(
+ 0L, CHAIN_ID, PqRegistryHash.loadScheduleFromGenesis(schedule));
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ System.clearProperty("aere.falcon.genesis");
+ System.clearProperty("aere.falcon.attachBlock");
+ System.clearProperty(PqSealWindowStore.PROPERTY_PERSIST);
+ PqSealCache.instance().disablePersistence();
+ PqSealCache.instance().clear();
+ PqAnchorProducer.useConfigForTesting(null);
+ resetFalconSingleton();
+ }
+
+ @Test
+ public void theWindowSurvivesARestartAndOnlyCanonicalBlocksComeBack() {
+ assertThat(FalconSealSupport.instance().genesisAnchored()).isTrue();
+ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
+
+ // Ten heights heard live, three genuine seals each. A height reaches the journal when the NEXT
+ // one begins, so after hearing HEAD the journal holds HEAD-9 .. HEAD-1 and the head file HEAD.
+ for (long n = HEAD - 9L; n <= HEAD; n++) {
+ for (int i = 0; i < 3; i++) {
+ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(i, header(n))));
+ }
+ }
+ // and one losing proposal at HEAD-4: heard, journaled, NOT canonical
+ final BlockHeader loser =
+ new BlockHeaderTestFixture().number(HEAD - 4L).extraData(Bytes.of(7, 7, 7)).buildHeader();
+ assertThat(loser.getHash()).isNotEqualTo(header(HEAD - 4L).getHash());
+
+ final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
+ assertThat(journal).isDirectory();
+ assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)).isEqualTo(9);
+ appendUnchecked(journal, loser, List.of(genuineSeal(0, loser)));
+
+ // THE RESTART: the in-memory window is gone
+ PqSealCache.instance().clear();
+ assertThat(PqSealCache.instance().sealCount(header(HEAD - 5L).getHash())).isZero();
+
+ // this node's chain holds every height except HEAD-7 (the negative: a height it cannot vouch for)
+ final LongFunction> canonical =
+ n -> n == HEAD - 7L ? Optional.empty() : Optional.of(header(n).getHash());
+ final int restored =
+ PqSealCache.instance()
+ .restoreWindowFromDisk(HEAD, canonical, PqSignerRegistry.falconSealSupport());
+
+ assertThat(restored)
+ .describedAs("nine journaled heights, minus the one the chain does not hold, three seals each")
+ .isEqualTo(8 * 3);
+ for (long n = HEAD - 9L; n < HEAD; n++) {
+ assertThat(PqSealCache.instance().sealCount(header(n).getHash()))
+ .describedAs("height %d", n)
+ .isEqualTo(n == HEAD - 7L ? 0 : 3);
+ }
+ assertThat(PqSealCache.instance().sealCount(loser.getHash()))
+ .describedAs("a frame for a hash that is not canonical at its height never comes back")
+ .isZero();
+ assertThat(PqSealCache.instance().sealsFor(HEAD - 2L, header(HEAD - 2L).getHash()))
+ .extracting(FalconSeal::getValidatorIndex)
+ .containsExactly(0, 1, 2);
+ }
+
+ @Test
+ public void aForgedSealInTheJournalNeverEntersTheWindow() throws Exception {
+ final BlockHeader block = header(HEAD - 3L);
+ final FalconSeal genuine0 = genuineSeal(0, block);
+ final FalconSeal genuine1 = genuineSeal(1, block);
+ final Bytes32 anotherBlocksMessage =
+ PqAnchor.commitMessage(CHAIN_ID, block.getNumber() - 1L, Bytes32.leftPad(Bytes.of(9)));
+ final byte[] randomBytes = new byte[genuine0.getSignature().size()];
+ SecureRandomProvider.createSecureRandom().nextBytes(randomBytes);
+ final List all =
+ List.of(
+ genuine0,
+ genuine1,
+ new FalconSeal(2, Bytes.wrap(falconSign(privateKeys.get(2), anotherBlocksMessage))),
+ new FalconSeal(3, Bytes.wrap(randomBytes)),
+ new FalconSeal(4, genuine0.getSignature()));
+ final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
+ PqSealWindowStore.append(
+ journal, CHAIN_ID, PqSealWindowStore.heard(block.getNumber(), block.getHash().getBytes(), all));
+
+ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
+ final int restored =
+ PqSealCache.instance()
+ .restoreWindowFromDisk(
+ HEAD, n -> Optional.of(header(n).getHash()), PqSignerRegistry.falconSealSupport());
+
+ assertThat(restored)
+ .describedAs(
+ "THE LOAD-BEARING ASSERTION. Five seals were in the frame and only the two genuine ones "
+ + "may come out: a signature over another block's message, random bytes and a "
+ + "genuine seal re-labelled under another index all die at the verifier. Take the "
+ + "verifier out of restoreWindowFromDisk and this line goes red, which is what the "
+ + "build-time negative control proves.")
+ .isEqualTo(2);
+ assertThat(PqSealCache.instance().sealsFor(block.getNumber(), block.getHash()))
+ .extracting(FalconSeal::getValidatorIndex)
+ .containsExactly(0, 1);
+ }
+
+ @Test
+ public void aTornTailOrAFlippedByteCostsOnlyWhatFollowsIt() throws Exception {
+ final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
+ // five heights in ONE segment (64 heights per segment; HEAD-5 .. HEAD-1 share 4992/64 = 78)
+ for (long n = HEAD - 5L; n < HEAD; n++) {
+ PqSealWindowStore.append(
+ journal,
+ CHAIN_ID,
+ PqSealWindowStore.heard(n, header(n).getHash().getBytes(), List.of(genuineSeal(0, header(n)))));
+ }
+ final Path segment = PqSealWindowStore.segmentFor(journal, HEAD - 1L);
+ assertThat(PqSealWindowStore.segmentFor(journal, HEAD - 5L)).isEqualTo(segment);
+ final byte[] whole = Files.readAllBytes(segment);
+ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
+
+ // positive control first: the undamaged journal gives all five
+ assertThat(restoreAll()).isEqualTo(5);
+
+ // (a) torn tail: the node died in the middle of the fifth append
+ Files.write(segment, java.util.Arrays.copyOf(whole, whole.length - 40));
+ PqSealCache.instance().clear();
+ assertThat(restoreAll()).describedAs("the four whole frames before the torn one").isEqualTo(4);
+
+ // (b) one byte flipped inside the THIRD frame: the checksum catches it, frames one and two live
+ final int frame = whole.length / 5;
+ final byte[] flipped = whole.clone();
+ flipped[2 * frame + frame / 2] ^= 0x20;
+ Files.write(segment, flipped);
+ PqSealCache.instance().clear();
+ assertThat(restoreAll()).describedAs("the two frames before the damaged one").isEqualTo(2);
+
+ // (c) garbage, empty, a directory where the segment should be: nothing, and no exception
+ Files.write(segment, new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9});
+ PqSealCache.instance().clear();
+ assertThat(restoreAll()).isZero();
+ Files.write(segment, new byte[0]);
+ assertThat(restoreAll()).isZero();
+ Files.delete(segment);
+ Files.createDirectory(segment);
+ assertThatCode(this::restoreAll).doesNotThrowAnyException();
+ assertThat(restoreAll()).isZero();
+ }
+
+ @Test
+ public void aJournalFromAnotherChainOrOutsideTheWindowIsIgnored() throws Exception {
+ final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
+ final BlockHeader inWindow = header(HEAD - 10L);
+ final BlockHeader tooOld = header(HEAD - PqSealCache.instance().retentionBlocks() - 50L);
+ PqSealWindowStore.append(
+ journal,
+ CHAIN_ID + 1L,
+ PqSealWindowStore.heard(
+ inWindow.getNumber(), inWindow.getHash().getBytes(), List.of(genuineSeal(0, inWindow))));
+ PqSealWindowStore.append(
+ journal,
+ CHAIN_ID,
+ PqSealWindowStore.heard(
+ tooOld.getNumber(), tooOld.getHash().getBytes(), List.of(genuineSeal(0, tooOld))));
+ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
+ assertThat(restoreAll())
+ .describedAs("another chain's frame and a height below the window: neither comes back")
+ .isZero();
+ // positive pair: the same height, this chain
+ PqSealWindowStore.append(
+ journal,
+ CHAIN_ID,
+ PqSealWindowStore.heard(
+ inWindow.getNumber(), inWindow.getHash().getBytes(), List.of(genuineSeal(0, inWindow))));
+ assertThat(restoreAll()).isEqualTo(1);
+ }
+
+ @Test
+ public void oldSegmentsAreDeletedSoTheFootprintStaysBounded() throws Exception {
+ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
+ final long window = PqSealCache.instance().retentionBlocks();
+ final long heights = window + 5L * PqSealWindowStore.SEGMENT_BLOCKS;
+ final FalconSeal one = new FalconSeal(0, Bytes.wrap(new byte[650]));
+ for (long n = HEAD; n < HEAD + heights; n++) {
+ PqSealCache.instance().record(n, Hash.hash(Bytes.ofUnsignedLong(n)), List.of(one));
+ }
+ final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
+ final long highest = HEAD + heights - 1L;
+ try (Stream files = Files.list(journal)) {
+ final List segments = files.toList();
+ assertThat(segments.size())
+ .describedAs("segments kept: the window plus the one being written plus one of slack")
+ .isLessThanOrEqualTo((int) (window / PqSealWindowStore.SEGMENT_BLOCKS) + 2);
+ assertThat(segments)
+ .describedAs("no segment wholly below the window is left behind")
+ .doesNotContain(PqSealWindowStore.segmentFor(journal, highest - window - 2L * PqSealWindowStore.SEGMENT_BLOCKS));
+ assertThat(segments).contains(PqSealWindowStore.segmentFor(journal, highest - 1L));
+ }
+ }
+
+ @Test
+ public void theJournalIsOffWhenPersistenceIsOffOrThePropertySaysSo() throws Exception {
+ final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
+ // persistence never enabled: memory only, exactly as before this class existed
+ for (long n = HEAD; n < HEAD + 3L; n++) {
+ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n))));
+ }
+ assertThat(journal).doesNotExist();
+
+ // the operator's switch
+ System.setProperty(PqSealWindowStore.PROPERTY_PERSIST, "false");
+ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
+ for (long n = HEAD + 3L; n < HEAD + 6L; n++) {
+ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n))));
+ }
+ assertThat(journal).doesNotExist();
+ assertThat(PqSealStore.fileIn(dataDirectory))
+ .describedAs("the head file, which liveness depends on, is NOT governed by this switch")
+ .exists();
+
+ // positive pair: the switch back on
+ System.clearProperty(PqSealWindowStore.PROPERTY_PERSIST);
+ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
+ for (long n = HEAD + 6L; n < HEAD + 9L; n++) {
+ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n))));
+ }
+ assertThat(journal).isDirectory();
+ assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)).isGreaterThanOrEqualTo(2);
+ }
+
+ @Test
+ public void aRestoredWindowIsNotAppendedASecondTime() {
+ PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
+ for (long n = HEAD - 4L; n <= HEAD; n++) {
+ PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n))));
+ }
+ final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
+ assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)).isEqualTo(4);
+
+ PqSealCache.instance().clear();
+ PqSealCache.instance()
+ .restoreFromDisk(HEAD, header(HEAD).getHash(), PqSignerRegistry.falconSealSupport());
+ assertThat(restoreAll()).isEqualTo(4);
+
+ // the chain moves on: exactly ONE new frame (the head, which was only in the head file)
+ PqSealCache.instance()
+ .record(HEAD + 1L, header(HEAD + 1L).getHash(), List.of(genuineSeal(0, header(HEAD + 1L))));
+ assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID))
+ .describedAs("four restored heights are not written again; the head is written once")
+ .isEqualTo(5);
+ }
+
+ @Test
+ public void whatOneAppendCostsAgainstTheBlockInterval() throws Exception {
+ final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
+ final List ten = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ ten.add(new FalconSeal(i, Bytes.wrap(new byte[660])));
+ }
+ final int rounds = 200;
+ final long t0 = System.nanoTime();
+ for (int r = 0; r < rounds; r++) {
+ PqSealWindowStore.append(
+ journal, CHAIN_ID, PqSealWindowStore.heard(HEAD + r, Bytes32.leftPad(Bytes.of(r)), ten));
+ }
+ final double meanMs = (System.nanoTime() - t0) / 1_000_000.0 / rounds;
+ System.out.printf(
+ "AERE FEREASTRA-SIGILII: one append of 10 seals costs %.3f ms on average, %.2f%% of a %d ms block%n",
+ meanMs, 100.0 * meanMs / BLOCK_INTERVAL_MS, BLOCK_INTERVAL_MS);
+ assertThat(meanMs)
+ .describedAs("an append on the consensus thread must stay far below the block interval")
+ .isLessThan(BLOCK_INTERVAL_MS * 0.05);
+ }
+
+ /**
+ * THE SHARED VECTOR, literal for literal in the Nethermind-derived client's
+ * AereAip21SealWindowJournalProofTests: the two clients write the SAME frame payload. Chain 28001,
+ * block 5, hash 32 x 0xaa, one seal (index 1, signature 0x0102). The frame around it is
+ * length(4, big endian) || payload || crc32(payload)(4, big endian), java.util.zip.CRC32.
+ */
+ @Test
+ public void theBytesOnDiskAreTheOnesTheOtherClientWrites() {
+ final Bytes hash = Bytes.fromHexString("0x" + "aa".repeat(32));
+ final byte[] payload =
+ PqSealWindowStore.encode(
+ 28_001L, PqSealWindowStore.heard(5L, hash, List.of(new FalconSeal(1, Bytes.of(1, 2)))));
+ assertThat(Bytes.wrap(payload).toUnprefixedHexString())
+ .isEqualTo(
+ "f84094414552452d50512d5345414c57494e444f572d31826d6105a0"
+ + "aa".repeat(32)
+ + "c5c401820102");
+ final java.util.zip.CRC32 crc = new java.util.zip.CRC32();
+ crc.update("123456789".getBytes(java.nio.charset.StandardCharsets.US_ASCII));
+ assertThat(crc.getValue()).isEqualTo(0xCBF43926L);
+ }
+
+ private int restoreAll() {
+ return PqSealCache.instance()
+ .restoreWindowFromDisk(
+ HEAD, n -> Optional.of(header(n).getHash()), PqSignerRegistry.falconSealSupport());
+ }
+
+ private BlockHeader header(final long number) {
+ return chain.computeIfAbsent(
+ number, n -> new BlockHeaderTestFixture().number(n).buildHeader());
+ }
+
+ private FalconSeal genuineSeal(final int index, final BlockHeader block) {
+ final Bytes32 m =
+ PqAnchor.commitMessage(CHAIN_ID, block.getNumber(), block.getHash().getBytes());
+ return new FalconSeal(index, Bytes.wrap(falconSign(privateKeys.get(index), m)));
+ }
+
+ private static void appendUnchecked(
+ final Path journal, final BlockHeader block, final List seals) {
+ try {
+ PqSealWindowStore.append(
+ journal,
+ CHAIN_ID,
+ PqSealWindowStore.heard(block.getNumber(), block.getHash().getBytes(), seals));
+ } catch (final java.io.IOException e) {
+ throw new java.io.UncheckedIOException(e);
+ }
+ }
+
+ private static byte[] falconSign(final FalconPrivateKeyParameters key, final Bytes32 m) {
+ final FalconSigner signer = new FalconSigner();
+ signer.init(true, key);
+ return signer.generateSignature(m.toArray());
+ }
+
+ private static void resetFalconSingleton() throws Exception {
+ final Field f = FalconSealSupport.class.getDeclaredField("instance");
+ f.setAccessible(true);
+ f.set(null, null);
+ }
+}
diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Proposal.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Proposal.java
new file mode 100644
index 0000000..64ece7d
--- /dev/null
+++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Proposal.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/aere-bft-bounded-message-lists.patch.
+ */
+package org.hyperledger.besu.consensus.qbft.core.messagewrappers;
+
+import org.hyperledger.besu.consensus.common.bft.messagewrappers.BftMessage;
+import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
+import org.hyperledger.besu.consensus.qbft.core.payload.PreparePayload;
+import org.hyperledger.besu.consensus.qbft.core.payload.ProposalPayload;
+import org.hyperledger.besu.consensus.qbft.core.payload.RoundChangePayload;
+import org.hyperledger.besu.consensus.qbft.core.types.QbftBlock;
+import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec;
+import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
+import org.hyperledger.besu.ethereum.rlp.RLP;
+import org.hyperledger.besu.ethereum.rlp.RLPInput;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.tuweni.bytes.Bytes;
+
+/** The Proposal. */
+public class Proposal extends BftMessage {
+
+ private final List> roundChanges;
+ private final List> prepares;
+
+ /**
+ * Instantiates a new Proposal.
+ *
+ * @param payload the payload
+ * @param roundChanges the round changes
+ * @param prepares the prepares
+ */
+ public Proposal(
+ final SignedData payload,
+ final List> roundChanges,
+ final List> prepares) {
+ super(payload);
+ this.roundChanges = roundChanges;
+ this.prepares = prepares;
+ }
+
+ /**
+ * Gets round changes.
+ *
+ * @return the round changes
+ */
+ public List> getRoundChanges() {
+ return roundChanges;
+ }
+
+ /**
+ * Gets list of Prepare payload.
+ *
+ * @return the list of Prepare payload
+ */
+ public List> getPrepares() {
+ return prepares;
+ }
+
+ /**
+ * Gets block.
+ *
+ * @return the block
+ */
+ public QbftBlock getBlock() {
+ return getPayload().getProposedBlock();
+ }
+
+ /**
+ * Gets block access list.
+ *
+ * @return the block access list
+ */
+ public Optional getBlockAccessList() {
+ return getPayload().getBlockAccessList();
+ }
+
+ @Override
+ public Bytes encode() {
+ final BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
+ rlpOut.startList();
+ getSignedPayload().writeTo(rlpOut);
+
+ rlpOut.startList();
+ rlpOut.writeList(roundChanges, SignedData::writeTo);
+ rlpOut.writeList(prepares, SignedData::writeTo);
+ rlpOut.endList();
+
+ rlpOut.endList();
+ return rlpOut.encoded();
+ }
+
+ /**
+ * Decode.
+ *
+ * @param data the data
+ * @param blockEncoder the qbft block encoder
+ * @return the proposal
+ */
+ public static Proposal decode(final Bytes data, final QbftBlockCodec blockEncoder) {
+ final RLPInput rlpIn = RLP.input(data);
+ rlpIn.enterList();
+ final SignedData payload =
+ readPayload(rlpIn, rlpInput -> ProposalPayload.readFrom(rlpInput, blockEncoder));
+
+ rlpIn.enterList();
+ final List> roundChanges =
+ rlpIn.readList(r -> readPayload(r, RoundChangePayload::readFrom), MAX_LIST_ENTRIES);
+ final List> prepares =
+ rlpIn.readList(r -> readPayload(r, PreparePayload::readFrom), MAX_LIST_ENTRIES);
+ rlpIn.leaveList();
+
+ rlpIn.leaveList();
+ return new Proposal(payload, roundChanges, prepares);
+ }
+}
diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/RoundChange.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/RoundChange.java
new file mode 100644
index 0000000..5d7cfce
--- /dev/null
+++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/RoundChange.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/aere-bft-bounded-message-lists.patch.
+ */
+package org.hyperledger.besu.consensus.qbft.core.messagewrappers;
+
+import org.hyperledger.besu.consensus.common.bft.messagewrappers.BftMessage;
+import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
+import org.hyperledger.besu.consensus.qbft.core.payload.PreparePayload;
+import org.hyperledger.besu.consensus.qbft.core.payload.PreparedRoundMetadata;
+import org.hyperledger.besu.consensus.qbft.core.payload.RoundChangePayload;
+import org.hyperledger.besu.consensus.qbft.core.types.QbftBlock;
+import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec;
+import org.hyperledger.besu.ethereum.core.encoding.BlockAccessListDecoder;
+import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList;
+import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
+import org.hyperledger.besu.ethereum.rlp.RLP;
+import org.hyperledger.besu.ethereum.rlp.RLPInput;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.tuweni.bytes.Bytes;
+
+/** The Round change payload message. */
+public class RoundChange extends BftMessage {
+
+ private final Optional proposedBlock;
+ private final Optional blockAccessList;
+ private final QbftBlockCodec blockEncoder;
+ private final List> prepares;
+
+ /**
+ * Instantiates a new Round change.
+ *
+ * @param payload the payload
+ * @param proposedBlock the proposed block
+ * @param blockAccessList the block access list
+ * @param blockEncoder the qbft block encoder
+ * @param prepares the prepares
+ */
+ public RoundChange(
+ final SignedData payload,
+ final Optional proposedBlock,
+ final Optional blockAccessList,
+ final QbftBlockCodec blockEncoder,
+ final List> prepares) {
+ super(payload);
+ this.proposedBlock = proposedBlock;
+ this.blockAccessList = blockAccessList;
+ this.blockEncoder = blockEncoder;
+ this.prepares = prepares;
+ }
+
+ /**
+ * Gets proposed block.
+ *
+ * @return the proposed block
+ */
+ public Optional getProposedBlock() {
+ return proposedBlock;
+ }
+
+ /**
+ * Gets block access list.
+ *
+ * @return the block access list
+ */
+ public Optional getBlockAccessList() {
+ return blockAccessList;
+ }
+
+ /**
+ * Gets list of Prepare payload signed data.
+ *
+ * @return the prepares
+ */
+ public List> getPrepares() {
+ return prepares;
+ }
+
+ /**
+ * Gets prepared round metadata.
+ *
+ * @return the prepared round metadata
+ */
+ public Optional getPreparedRoundMetadata() {
+ return getPayload().getPreparedRoundMetadata();
+ }
+
+ /**
+ * Gets prepared round.
+ *
+ * @return the prepared round
+ */
+ public Optional getPreparedRound() {
+ return getPayload().getPreparedRoundMetadata().map(PreparedRoundMetadata::getPreparedRound);
+ }
+
+ @Override
+ public Bytes encode() {
+ final BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
+ rlpOut.startList();
+ getSignedPayload().writeTo(rlpOut);
+ proposedBlock.ifPresentOrElse(pb -> blockEncoder.writeTo(pb, rlpOut), rlpOut::writeEmptyList);
+ blockAccessList.ifPresentOrElse((bal) -> bal.writeTo(rlpOut), rlpOut::writeNull);
+ rlpOut.writeList(prepares, SignedData::writeTo);
+ rlpOut.endList();
+ return rlpOut.encoded();
+ }
+
+ /**
+ * Decode.
+ *
+ * @param data the data
+ * @param blockEncoder the qbft block encoder
+ * @return the round change
+ */
+ public static RoundChange decode(final Bytes data, final QbftBlockCodec blockEncoder) {
+
+ final RLPInput rlpIn = RLP.input(data);
+ rlpIn.enterList();
+ final SignedData payload = readPayload(rlpIn, RoundChangePayload::readFrom);
+
+ final Optional block;
+ if (rlpIn.nextIsList() && rlpIn.nextSize() == 0) {
+ rlpIn.skipNext();
+ block = Optional.empty();
+ } else {
+ block = Optional.of(blockEncoder.readFrom(rlpIn));
+ }
+
+ final Optional blockAccessList = readBlockAccessList(rlpIn);
+
+ final List> prepares =
+ rlpIn.readList(r -> readPayload(r, PreparePayload::readFrom), MAX_LIST_ENTRIES);
+ rlpIn.leaveList();
+
+ return new RoundChange(payload, block, blockAccessList, blockEncoder, prepares);
+ }
+
+ private static Optional readBlockAccessList(final RLPInput rlpIn) {
+ if (rlpIn.isEndOfCurrentList()) {
+ return Optional.empty();
+ }
+ if (!rlpIn.nextIsNull()) {
+ return Optional.of(BlockAccessListDecoder.decode(rlpIn));
+ }
+ rlpIn.skipNext();
+ return Optional.empty();
+ }
+}
diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
index 189760f..8dc438b 100644
--- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
+++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
@@ -112,7 +112,7 @@ public class QbftRound {
// 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
+ // ms measured at a reading node of the network): 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
diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRoundFactory.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRoundFactory.java
new file mode 100644
index 0000000..a797951
--- /dev/null
+++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRoundFactory.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
+ */
+package org.hyperledger.besu.consensus.qbft.core.statemachine;
+
+import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
+import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
+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.QbftBlockCreator;
+import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCreatorFactory;
+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.QbftFinalState;
+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.MessageValidatorFactory;
+import org.hyperledger.besu.util.Subscribers;
+
+/** The Qbft round factory. */
+public class QbftRoundFactory {
+
+ private final QbftFinalState finalState;
+ private final QbftBlockCreatorFactory blockCreatorFactory;
+ private final QbftBlockInterface blockInterface;
+ private final QbftProtocolSchedule protocolSchedule;
+ private final Subscribers minedBlockObservers;
+ private final MessageValidatorFactory messageValidatorFactory;
+ private final MessageFactory messageFactory;
+
+ /**
+ * Instantiates a new Qbft round factory.
+ *
+ * @param finalState the final state
+ * @param blockInterface the block interface
+ * @param protocolSchedule the protocol schedule
+ * @param minedBlockObservers the mined block observers
+ * @param messageValidatorFactory the message validator factory
+ * @param messageFactory the message factory
+ */
+ public QbftRoundFactory(
+ final QbftFinalState finalState,
+ final QbftBlockInterface blockInterface,
+ final QbftProtocolSchedule protocolSchedule,
+ final Subscribers minedBlockObservers,
+ final MessageValidatorFactory messageValidatorFactory,
+ final MessageFactory messageFactory) {
+ this.finalState = finalState;
+ this.blockCreatorFactory = finalState.getBlockCreatorFactory();
+ this.blockInterface = blockInterface;
+ this.protocolSchedule = protocolSchedule;
+ this.minedBlockObservers = minedBlockObservers;
+ this.messageValidatorFactory = messageValidatorFactory;
+ this.messageFactory = messageFactory;
+ }
+
+ /**
+ * Create new round qbft round.
+ *
+ * @param parentHeader the parent header
+ * @param round the round
+ * @return the qbft round
+ */
+ public QbftRound createNewRound(final QbftBlockHeader parentHeader, final int round) {
+ long nextBlockHeight = parentHeader.getNumber() + 1;
+ final ConsensusRoundIdentifier roundIdentifier =
+ new ConsensusRoundIdentifier(nextBlockHeight, round);
+
+ final RoundState roundState =
+ new RoundState(
+ roundIdentifier,
+ finalState.getQuorum(),
+ messageValidatorFactory.createMessageValidator(roundIdentifier, parentHeader));
+
+ return createNewRoundWithState(parentHeader, roundState);
+ }
+
+ /**
+ * Create new Qbft round with state.
+ *
+ * @param parentHeader the parent header
+ * @param roundState the round state
+ * @return the qbft round
+ */
+ public QbftRound createNewRoundWithState(
+ final QbftBlockHeader parentHeader, final RoundState roundState) {
+ final QbftBlockCreator blockCreator =
+ blockCreatorFactory.create(roundState.getRoundIdentifier().getRoundNumber());
+
+ // TODO(tmm): Why is this created everytime?!
+ final QbftMessageTransmitter messageTransmitter =
+ new QbftMessageTransmitter(messageFactory, finalState.getValidatorMulticaster());
+
+ return new QbftRound(
+ roundState,
+ blockCreator,
+ blockInterface,
+ protocolSchedule,
+ minedBlockObservers,
+ finalState.getNodeKey(),
+ AerePqIdentity.local(finalState.getLocalAddress(), parentHeader.getNumber() + 1L),
+ messageFactory,
+ messageTransmitter,
+ finalState.getRoundTimer(),
+ parentHeader);
+ }
+}
diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/AereD434ListBoundsTest.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/AereD434ListBoundsTest.java
new file mode 100644
index 0000000..889e0e7
--- /dev/null
+++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/AereD434ListBoundsTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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), adapted from the tests of the
+ * ported upstream fix hyperledger/besu dd4101c9ec ("bound prepares/round-change lists at decode").
+ */
+package org.hyperledger.besu.consensus.qbft.core.messagewrappers;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
+import org.hyperledger.besu.consensus.common.bft.messagewrappers.BftMessage;
+import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
+import org.hyperledger.besu.consensus.qbft.core.payload.PreparePayload;
+import org.hyperledger.besu.consensus.qbft.core.payload.RoundChangePayload;
+import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec;
+import org.hyperledger.besu.cryptoservices.NodeKey;
+import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
+import org.hyperledger.besu.datatypes.Hash;
+import org.hyperledger.besu.ethereum.rlp.RLPException;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.apache.tuweni.bytes.Bytes32;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+/**
+ * AERE D-434: a RoundChange whose prepares list holds more than BftMessage.MAX_LIST_ENTRIES (512) entries is refused at DECODE, before
+ * any signature or author check; before the port any peer could make a node allocate and verify an arbitrarily long list. With ten
+ * validators an honest list never exceeds ten, so the bound never binds an honest network.
+ */
+class AereD434ListBoundsTest {
+
+ private static final QbftBlockCodec CODEC = Mockito.mock(QbftBlockCodec.class);
+
+ private static Bytes roundChangeWith(final int prepares) {
+ final NodeKey key = NodeKeyUtils.generate();
+ final RoundChangePayload rc = new RoundChangePayload(new ConsensusRoundIdentifier(1, 0), Optional.empty());
+ final SignedData signedRc = SignedData.create(rc, key.sign(Bytes32.wrap(rc.hashForSignature().getBytes())));
+ final PreparePayload p = new PreparePayload(new ConsensusRoundIdentifier(1, 0), Hash.ZERO);
+ final SignedData one = SignedData.create(p, key.sign(Bytes32.wrap(p.hashForSignature().getBytes())));
+ return new RoundChange(signedRc, Optional.empty(), Optional.empty(), CODEC, Collections.nCopies(prepares, one)).encode();
+ }
+
+ @Test
+ void aPreparesListAboveTheBoundIsRefusedAtDecode() {
+ final Bytes oversized = roundChangeWith(BftMessage.MAX_LIST_ENTRIES + 1);
+ assertThatThrownBy(() -> RoundChange.decode(oversized, CODEC))
+ .isInstanceOf(RLPException.class)
+ .hasMessageContaining("exceeds the maximum permitted size");
+ }
+
+ @Test
+ void aPreparesListAtTheBoundStillDecodes() {
+ // positive control: the fixture decodes, and the bound is inclusive
+ assertThat(RoundChange.decode(roundChangeWith(BftMessage.MAX_LIST_ENTRIES), CODEC).getPrepares()).hasSize(BftMessage.MAX_LIST_ENTRIES);
+ }
+}
diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftFinalStateImpl.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftFinalStateImpl.java
new file mode 100644
index 0000000..03d2c64
--- /dev/null
+++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftFinalStateImpl.java
@@ -0,0 +1,196 @@
+/*
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
+ */
+package org.hyperledger.besu.consensus.qbft.adaptor;
+
+import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
+import org.hyperledger.besu.consensus.common.bft.BftHelpers;
+import org.hyperledger.besu.consensus.common.bft.BlockTimer;
+import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
+import org.hyperledger.besu.consensus.common.bft.RoundTimer;
+import org.hyperledger.besu.consensus.common.bft.blockcreation.ProposerSelector;
+import org.hyperledger.besu.consensus.common.bft.network.ValidatorMulticaster;
+import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
+import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCreatorFactory;
+import org.hyperledger.besu.consensus.qbft.core.types.QbftFinalState;
+import org.hyperledger.besu.cryptoservices.NodeKey;
+import org.hyperledger.besu.datatypes.Address;
+
+import java.time.Clock;
+import java.util.Collection;
+
+/** Besu implementation of QbftFinalState for maintaining the state of a QBFT network. */
+public class QbftFinalStateImpl implements QbftFinalState {
+ private final ValidatorProvider validatorProvider;
+ private final NodeKey nodeKey;
+ private final Address localAddress;
+ private final ProposerSelector proposerSelector;
+ private final ValidatorMulticaster validatorMulticaster;
+ private final RoundTimer roundTimer;
+ private final BlockTimer blockTimer;
+ private final QbftBlockCreatorFactory blockCreatorFactory;
+ private final Clock clock;
+
+ /**
+ * Constructs a new QBFT final state.
+ *
+ * @param validatorProvider the validator provider
+ * @param nodeKey the node key
+ * @param localAddress the local address
+ * @param proposerSelector the proposer selector
+ * @param validatorMulticaster the validator multicaster
+ * @param roundTimer the round timer
+ * @param blockTimer the block timer
+ * @param blockCreatorFactory the block creator factory
+ * @param clock the clock
+ */
+ public QbftFinalStateImpl(
+ final ValidatorProvider validatorProvider,
+ final NodeKey nodeKey,
+ final Address localAddress,
+ final ProposerSelector proposerSelector,
+ final ValidatorMulticaster validatorMulticaster,
+ final RoundTimer roundTimer,
+ final BlockTimer blockTimer,
+ final QbftBlockCreatorFactory blockCreatorFactory,
+ final Clock clock) {
+ this.validatorProvider = validatorProvider;
+ this.nodeKey = nodeKey;
+ this.localAddress = localAddress;
+ this.proposerSelector = proposerSelector;
+ this.validatorMulticaster = validatorMulticaster;
+ this.roundTimer = roundTimer;
+ this.blockTimer = blockTimer;
+ this.blockCreatorFactory = blockCreatorFactory;
+ this.clock = clock;
+ }
+
+ /**
+ * Gets validators.
+ *
+ * @return the validators
+ */
+ @Override
+ public Collection getValidators() {
+ return validatorProvider.getValidatorsAtHead();
+ }
+
+ /**
+ * Gets the validator multicaster.
+ *
+ * @return the validator multicaster
+ */
+ @Override
+ public ValidatorMulticaster getValidatorMulticaster() {
+ return validatorMulticaster;
+ }
+
+ /**
+ * Gets node key.
+ *
+ * @return the node key
+ */
+ @Override
+ public NodeKey getNodeKey() {
+ return nodeKey;
+ }
+
+ /**
+ * Gets local address.
+ *
+ * @return the local address
+ */
+ @Override
+ public Address getLocalAddress() {
+ return localAddress;
+ }
+
+ /**
+ * Is local node validator.
+ *
+ * @return the boolean
+ */
+ @Override
+ public boolean isLocalNodeValidator() {
+ return getValidators().stream().anyMatch(a -> AerePqIdentity.isLocal(a, localAddress));
+ }
+
+ /**
+ * Gets round timer.
+ *
+ * @return the round timer
+ */
+ @Override
+ public RoundTimer getRoundTimer() {
+ return roundTimer;
+ }
+
+ /**
+ * Gets block creator factory.
+ *
+ * @return the block creator factory
+ */
+ @Override
+ public QbftBlockCreatorFactory getBlockCreatorFactory() {
+ return blockCreatorFactory;
+ }
+
+ @Override
+ public int getQuorum() {
+ return BftHelpers.calculateRequiredValidatorQuorum(getValidators().size());
+ }
+
+ @Override
+ public BlockTimer getBlockTimer() {
+ return blockTimer;
+ }
+
+ /**
+ * Is local node proposer for round.
+ *
+ * @param roundIdentifier the round identifier
+ * @return the boolean
+ */
+ @Override
+ public boolean isLocalNodeProposerForRound(final ConsensusRoundIdentifier roundIdentifier) {
+ return getProposerForRound(roundIdentifier)
+ .equals(AerePqIdentity.local(localAddress, roundIdentifier.getSequenceNumber()));
+ }
+
+ /**
+ * Gets proposer for round.
+ *
+ * @param roundIdentifier the round identifier
+ * @return the proposer for round
+ */
+ public Address getProposerForRound(final ConsensusRoundIdentifier roundIdentifier) {
+ return proposerSelector.selectProposerForRound(roundIdentifier);
+ }
+
+ /**
+ * Gets clock.
+ *
+ * @return the clock
+ */
+ @Override
+ public Clock getClock() {
+ return clock;
+ }
+}
diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java
new file mode 100644
index 0000000..674b39e
--- /dev/null
+++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright ConsenSys AG.
+ *
+ * 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
+ *
+ * Modifications Copyright contributors to the Aere Network.
+ *
+ * This file was modified by contributors to the Aere Network, as required by section 4(b) of the
+ * Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
+ * found, as section 4(c) requires. The change itself is in patches/0007-aere-pq-finality.patch.
+ */
+package org.hyperledger.besu.consensus.qbft.jsonrpc;
+
+import org.hyperledger.besu.config.BftConfigOptions;
+import org.hyperledger.besu.consensus.common.BlockInterface;
+import org.hyperledger.besu.consensus.common.bft.BftContext;
+import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
+import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftDiscardValidatorVote;
+import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetPendingVotes;
+import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.AereGetPqFinality;
+import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetRequestTimeoutSeconds;
+import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetSignerMetrics;
+import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetValidatorsByBlockHash;
+import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetValidatorsByBlockNumber;
+import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftProposeValidatorVote;
+import org.hyperledger.besu.ethereum.ProtocolContext;
+import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis;
+import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
+import org.hyperledger.besu.ethereum.api.jsonrpc.methods.ApiGroupJsonRpcMethods;
+import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
+import org.hyperledger.besu.ethereum.core.MiningConfiguration;
+import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
+
+import java.util.Map;
+
+/** The Qbft json rpc methods. */
+public class QbftJsonRpcMethods extends ApiGroupJsonRpcMethods {
+
+ private final ProtocolContext context;
+ private final ValidatorProvider readOnlyValidatorProvider;
+ private final ProtocolSchedule protocolSchedule;
+ private final MiningConfiguration miningConfiguration;
+ private final BftConfigOptions bftConfig;
+
+ /**
+ * Instantiates a new Qbft json rpc methods.
+ *
+ * @param context the protocol context
+ * @param protocolSchedule the protocol schedule
+ * @param miningConfiguration the mining parameters
+ * @param readOnlyValidatorProvider the read only validator provider
+ * @param bftConfig the BFT config options, containing QBFT-specific settings
+ */
+ public QbftJsonRpcMethods(
+ final ProtocolContext context,
+ final ProtocolSchedule protocolSchedule,
+ final MiningConfiguration miningConfiguration,
+ final ValidatorProvider readOnlyValidatorProvider,
+ final BftConfigOptions bftConfig) {
+ this.context = context;
+ this.readOnlyValidatorProvider = readOnlyValidatorProvider;
+ this.protocolSchedule = protocolSchedule;
+ this.miningConfiguration = miningConfiguration;
+ this.bftConfig = bftConfig;
+ }
+
+ @Override
+ protected String getApiGroup() {
+ return RpcApis.QBFT.name();
+ }
+
+ @Override
+ protected Map create() {
+ final BlockchainQueries blockchainQueries =
+ new BlockchainQueries(
+ protocolSchedule,
+ context.getBlockchain(),
+ context.getWorldStateArchive(),
+ miningConfiguration);
+ final BftContext bftContext = context.getConsensusContext(BftContext.class);
+ final BlockInterface blockInterface = bftContext.getBlockInterface();
+ final ValidatorProvider validatorProvider = bftContext.getValidatorProvider();
+
+ Map methods =
+ mapOf(
+ new QbftProposeValidatorVote(validatorProvider),
+ new QbftGetValidatorsByBlockNumber(blockchainQueries, readOnlyValidatorProvider),
+ new QbftDiscardValidatorVote(validatorProvider),
+ new QbftGetValidatorsByBlockHash(context.getBlockchain(), readOnlyValidatorProvider),
+ new QbftGetSignerMetrics(readOnlyValidatorProvider, blockInterface, blockchainQueries),
+ new QbftGetPendingVotes(validatorProvider),
+ new QbftGetRequestTimeoutSeconds(bftConfig),
+ // AIP-21 (AERE): per-block post-quantum finality, non-consensus, verified against the anchored registry
+ new AereGetPqFinality(blockchainQueries, readOnlyValidatorProvider));
+
+ return methods;
+ }
+}
diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinality.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinality.java
new file mode 100644
index 0000000..7fc5d24
--- /dev/null
+++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinality.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright contributors to Besu / AERE Network.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.hyperledger.besu.consensus.qbft.jsonrpc.methods;
+
+import org.hyperledger.besu.consensus.common.bft.BftHelpers;
+import org.hyperledger.besu.consensus.common.bft.FalconSeal;
+import org.hyperledger.besu.consensus.common.bft.PqSealCache;
+import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
+import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
+import org.hyperledger.besu.consensus.common.bft.SealSchemes;
+import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
+import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
+import org.hyperledger.besu.datatypes.Hash;
+import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
+import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
+import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
+import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.AbstractBlockParameterMethod;
+import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameter;
+import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException;
+import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
+import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeSet;
+
+import org.apache.tuweni.bytes.Bytes;
+
+/**
+ * AIP-21 (AERE, 2026-09-18), first half: the post-quantum finality of ONE block, verifiable per block,
+ * without growing the header.
+ *
+ *
WHAT IT ANSWERS. Every block of chain 2800 is committed with a Falcon-512 seal on each Commit
+ * message (enforced since H1, 2026-09-05), and every validator keeps the seals it heard for recent
+ * blocks in {@link PqSealCache}. Those seals never reach the header (only every 128th block carries a
+ * certificate, over its parent), so until now the immediate post-quantum finality of a block was true
+ * and NOT observable. This method exposes it: for a block it returns the seals heard, re-verifies each
+ * one right now against the anchored registry over the exact message the validators signed, and gives
+ * the verdict "verified distinct signers >= the chain's commit quorum".
+ *
+ *
WHAT IT DOES NOT CLAIM. Seals travel only on Commit messages, which validators send to
+ * validators: a reader node hears none and answers {@code finality: "unavailable"} honestly. The
+ * retention window is the cache's (system property {@code aere.pq.finality.window}, blocks; default
+ * 256), so an older block answers "unavailable" too, and the permanent record for it is the anchor
+ * certificate. Hybrid extras (SLH-DSA seals on anchor parents) are listed as heard but NOT verified
+ * here ({@code verified: null}); their verification is the anchor certificate's job. Below the anchor
+ * form (never on a live chain today) the signed message is the round-specific commit digest, which
+ * cannot be rebuilt from the header, so the method says so instead of guessing.
+ *
+ *
NON-CONSENSUS. Nothing here changes what a block is; a node with or without this method imports
+ * the same chain. That is why it needs no activation height.
+ */
+public class AereGetPqFinality extends AbstractBlockParameterMethod {
+
+ /** Verifies one Falcon seal over the message signed for a block; injectable for tests. */
+ @FunctionalInterface
+ public interface SealVerifier {
+ /**
+ * Verify.
+ *
+ * @param blockNumber the height
+ * @param validatorIndex the signer index in the anchored registry
+ * @param message the message that was signed
+ * @param signature the Falcon signature
+ * @return true iff it verifies against the key bound at that height
+ */
+ boolean verify(long blockNumber, int validatorIndex, Bytes message, Bytes signature);
+ }
+
+ /** Rebuilds the message the seals of a block signed, when that is possible from the header. */
+ @FunctionalInterface
+ public interface MessageBuilder {
+ /**
+ * The signed message.
+ *
+ * @param blockNumber the height
+ * @param onchainHash the round-independent on-chain hash
+ * @return the message, or empty when the form at that height cannot be rebuilt from the header
+ */
+ Optional messageFor(long blockNumber, Hash onchainHash);
+ }
+
+ private final ValidatorProvider validatorProvider;
+ private final PqSealCache cache;
+ private final SealVerifier verifier;
+ private final MessageBuilder messageBuilder;
+
+ /**
+ * Production wiring: the process-wide seal cache, the anchored Falcon registry at the block's
+ * height, and the anchor-form commit message.
+ *
+ * @param blockchainQueries the blockchain queries
+ * @param validatorProvider the read-only validator provider
+ */
+ public AereGetPqFinality(
+ final BlockchainQueries blockchainQueries, final ValidatorProvider validatorProvider) {
+ this(
+ blockchainQueries,
+ validatorProvider,
+ PqSealCache.instance(),
+ (n, i, m, s) -> PqSignerRegistry.falconSealSupport().verifyAtHistoric(n, i, m, s),
+ (n, h) ->
+ PqAnchorProducer.sealMessageIsAnchorForm(n)
+ ? Optional.of(PqAnchorProducer.commitSealMessage(n, h.getBytes(), Hash.ZERO))
+ : Optional.empty());
+ }
+
+ /**
+ * Full wiring, for tests.
+ *
+ * @param blockchainQueries the blockchain queries
+ * @param validatorProvider the validator provider
+ * @param cache the seal cache to read
+ * @param verifier the seal verifier
+ * @param messageBuilder the signed-message builder
+ */
+ public AereGetPqFinality(
+ final BlockchainQueries blockchainQueries,
+ final ValidatorProvider validatorProvider,
+ final PqSealCache cache,
+ final SealVerifier verifier,
+ final MessageBuilder messageBuilder) {
+ super(blockchainQueries);
+ this.validatorProvider = validatorProvider;
+ this.cache = cache;
+ this.verifier = verifier;
+ this.messageBuilder = messageBuilder;
+ }
+
+ @Override
+ public String getName() {
+ return RpcMethod.AERE_GET_PQ_FINALITY.getMethodName();
+ }
+
+ @Override
+ protected BlockParameter blockParameter(final JsonRpcRequestContext request) {
+ try {
+ return request.getOptionalParameter(0, BlockParameter.class).orElse(BlockParameter.LATEST);
+ } catch (final JsonRpcParameterException e) {
+ throw new InvalidJsonRpcParameters(
+ "Invalid block parameter (index 0)", RpcErrorType.INVALID_BLOCK_PARAMS, e);
+ }
+ }
+
+ @Override
+ protected Object pendingResult(final JsonRpcRequestContext request) {
+ return describe(getBlockchainQueries().headBlockHeader());
+ }
+
+ @Override
+ protected Object resultByBlockNumber(final JsonRpcRequestContext request, final long blockNumber) {
+ return getBlockchainQueries().getBlockHeaderByNumber(blockNumber).map(this::describe).orElse(null);
+ }
+
+ /**
+ * The finality record of one block.
+ *
+ * @param header the block's header
+ * @return the JSON-shaped record
+ */
+ public Map describe(final BlockHeader header) {
+ final long number = header.getNumber();
+ final Hash hash = header.getHash();
+ final int validators = validatorProvider.getValidatorsForBlock(header).size();
+ final int quorum = validators == 0 ? 0 : BftHelpers.calculateRequiredValidatorQuorum(validators);
+ final Optional message = messageBuilder.messageFor(number, hash);
+ final List heard = cache.sealsFor(number, hash);
+ final List extras = cache.extrasFor(number, hash);
+
+ final List