diff --git a/LICENSING.md b/LICENSING.md index 7375c52..2f7be4e 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -123,7 +123,7 @@ the section 4(b) notice of changed files), and each keeps its original notice pe -## anchor/: every file that carries someone else's copyright (generated 2026-09-23) +## anchor/: every file that carries someone else's copyright (generated 2026-09-24) This list is **generated from the files this package actually ships**, not written by hand: the previous hand-written table fell behind the tree on 2026-09-12 and seven upstream files went @@ -132,9 +132,9 @@ repository, and the publisher refuses to ship a package whose list disagrees wit You do not need that tool to check the list: every row below names a file in this package, and `grep -i copyright` on it shows the same line. -Measured 2026-09-23 over `anchor/`: **198 files**. **90** carry someone -else's copyright alone, **54** carry a shared notice naming both upstream and us, and **11** name only -`contributors to the AERE Network`; 43 carry no copyright line at all. +Measured 2026-09-24 over `anchor/`: **202 files**. **90** carry someone +else's copyright alone, **57** carry a shared notice naming both upstream and us, and **11** name only +`contributors to the AERE Network`; 44 carry no copyright line at all. The first two groups are the table below. A shared notice is listed too: those are precisely the upstream files we changed, so section 4(b) has something to say about each of them. Every file below is Apache-2.0 material redistributed under section 4: it keeps its original @@ -150,6 +150,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar | `anchor/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/common/build.gradle` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | +| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqHeaderSeal.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java` | `Copyright ConsenSys AG.` | upstream | @@ -268,6 +269,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar | `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | +| `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealLogThrottleTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRuleRetirementTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | @@ -279,6 +281,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar | `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRuleTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRuleTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqForkGateFeedTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | +| `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetCode.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetStorageAt.java` | `Copyright ConsenSys AG.` | upstream | diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt index a86cc5c..6d8b135 100644 --- a/anchor/MANIFEST-sha256.txt +++ b/anchor/MANIFEST-sha256.txt @@ -5,15 +5,16 @@ computed them, and the same 276 files once produced two different aggregates her 329e4e2f93143cc8af092f0a951fa23619c12c4ec1793b053654f8e26e088da0 app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java 3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java -6bc5a9ae4dbeda9a26e3157cb61c938cc306c859ee597526107199e0c88166ae app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +1615ecaf19f75af3e91c27c029177bf0faf51839f5589878b8c3562371299865 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java 129493150c0b13e8020bdf50c0c1ae07268d4f73a69a821b303bf4894451fa5e app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java 728c29c299edf6011f2c2798cbe930a043112e0e06b924e7edb54284ca44a758 config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java a3b93f2602e9755d91358c9b3f473235f4d40ae23d912f797cdb03ed7e1348f9 consensus/common/build.gradle 68d635954ef33448a661256f50642eeeff2f7bb5874a54ffc2e64193c83fe391 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelay.java ed2e46aaf2e74661ad831dc31df0b6f795f28f4a94496a7d226feedf2a770fde consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java +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 -c28e3bf26d68ab7b764f6381bc678d5f7b60bfa7d02796e3ac34521c5c5bc6ed consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java -2e84bb48611d3da7c46bcd4018ae746cbf785dfcb16f495674a0f6d542e71e8e consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.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 3ab425b5b5f7d7c2199065a3905623d5c1c4c5922d892c6c4275415b0c69a2b4 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java db1e80115ce59c8281fbf046cb37e153a49fe017c1675afd200e8125d3f6b538 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealScheme.java @@ -121,12 +122,12 @@ d365bad33f3aee4597c630071ca1f727ddd8bb6ab6515fccbce0a07842037adf 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 -9c7e2698016f8cf19d3b6d78c6256ad5e8eabec06d9a877eee4e8c2dde423839 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java -00c8204e4b44acd469973d6bbc07992382fdd2d692f079a55de76f944a7432f8 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java -601b0cccf32f8ebe327c7581e0e9b3f9f67de49d9fb646326d6b49ef2138c641 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java +6a34f5e87b5c5c629f1f08d0dc88c336a3ab3c0ba8797d3b0ee477246f10f4c6 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java +4494f33ca028b60eaa093e6c24dd13706461e228f7f5a29e40e8917d1ef3b373 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java +eadc4a14e2f0fdf7c1d9667d67c59f709af7a86fccf51923de0248b73e223c21 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java 4c0811d182f5252bdde31560f514a518db58c8d0c82c748a6c0f5d68472470c6 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java a40621537e2b2024ab5586c9001f96fd81ffe3c3d5ce1e867a07a77584cf2173 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/MessageValidator.java -22c73f9cdfe5626347f34f173b7b60312e00970ccba49862686051b2dd84fe36 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java +60c0c68de44d277456f84452d2acaf93a4a239728f65af2fb55be49bbe417123 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java bacf232d9f3a9163ce6df3091c5e9161efcb26a73aecaead910e6952ee27bf86 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuard.java 5ab52433f770537e838b15e23abfc7b24eaa733ea26577451d5e32226d617879 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcement.java 742611d52091a6c138cdff60c5f7f0bfb2fc33e567f520f3e86878fc086e77d9 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqProposalEnforcement.java @@ -153,6 +154,7 @@ c1b31f5c357a1966cdd1e963369a1eb3078a381679c6adc8fb5206f754eca84b consensus/qbft c0853ed97c53d54951e25ad6d0b70c0299dedd859cc7c44da64e3751d0e0de33 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitPlumbingTest.java 01cefcd272ca54e1ebd37f606ec87cc884918c1d72289347a7c234cdca2840ff consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuardTest.java 52e75fd98b9d2ab3d6f87fc8ea1a638321e70d5eec5f14db615ee13755ba1287 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqHybridEnforcementTest.java +1834ed0950cdac7620091315e1dfd60f61cecd10def1196c69bf8a22db08f943 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqHybridRotationV3Test.java bca46ca91803c89eb18d670c740edb4e47ef0bca53a9b4b82ce0c8446e0d6c0e consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqMessageAgilityTest.java 5a47d247d7bcda77b57f5c906c3cee1af826785416012fafe9cfa49d63671388 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareAgilityTest.java 1939e33cc8ea81782e5e17d68e2b59c001e4207ed8ef07acd1d2a166a047dcb3 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcementTest.java @@ -163,9 +165,9 @@ dc9f9e862a11f0135d26976176a1d1adac3e84fd5f1e06572d8727262cb3374f consensus/qbft 2c612868dfdd61d86cadc4ca9b34faacf4e43aca88878feb72ac140e1321ef5d consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/ProposalPayloadValidatorPqWiringTest.java 83dd971f66ed63103d09db5283240556bf1db1e1925c2a07808c7f59d47433ad consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangeJustificationPqTest.java 8f6abb4297df964e934cfeabe51f365478e917b73cd6909f621c9e201e280aa5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidatorPqWiringTest.java -77568cb12200fd653772cd5bb93f989b012909fdb645d68fa39e8251138ec89e consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java -42b86cd888efe1aa00f9c9477566e353649c609c13a042e514185b57a6674a33 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java -103d46eecfc11c97fb59a1e0237a6b5b6d384a43ec45f5028bd225b8bf8e3f6c consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java +24ee993a415eea57c80fe2c9b839ce20140bfcc580e269e7b1182f71e960c1f0 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java +65ba290ae597b2217b86f2dd4b9db5faacc6e60ef7e36343a9d724c40b66dd73 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java +c870d39a19174328bc394658f4a365a61bd57fe7b0e28e6c5e4aaeb28bb509e5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java cfb0aad408ab620f28b09914ece7d8d3d09f9e6bbb503070e28d9ef39c023dee consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/blockcreation/QbftBlockCreatorFactory.java cbec6f4e280250ddefe5c79c453b59a07be6435e6f88d5614e92168c855e4693 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRule.java e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRule.java @@ -174,6 +176,7 @@ ab0fcd8722dcb76560f0ef8fda8af9c2b8b6ec9b468326aaf616c853526f7f81 consensus/qbft 0f348ac4fa519f27f52f631dd290eab6e84980739a09ec767c8ca69e4a4f8f64 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java a76a87987050ea6765e2a1f43d8f828ca8aec218c951e395e7acb62465e8b2fd consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRule.java 2add2a7733668e11b50ddd64b05bb37db26a18284a636bbe720c2cb1f6aa28ed consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRule.java +47dd0c84b2f1dba800ec843a0c06f85db86825e06493df13ffca3d5b5a9219dc consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java 8ac99e94c89f61f0281199cf369e282fc4f9fb1a3414a1650766e1d7548e76f5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java 5dcf5c22389c063e12363b83366b39bd3b2e79f36c68e777fb3ef11080f53ea9 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/AereIdentityHeaderTest.java 52b56f157500ae3527b7e55c51786915bb5d6980065668f1e7b8e297bf7125ef consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftAnchorRuleWiringTest.java @@ -189,6 +192,7 @@ a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft 9c4e6a56ecffa70ebb03f769eda8c161d70eed47935488b88cb30c1bafa15a73 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRuleTest.java 484bc74ff3d1e52d25631264d83af70c2b695e973c7360b0f3db471ab5c6613a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRuleTest.java 49c3fd97e7985966531928af173b6f38ac48ff9dc8b5be1106e5b7bfb9c7b2b6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqForkGateFeedTest.java +2db85c0c1b080123b252b6fb8896266d9d66753cf352826d3ed72c0790973abb consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java 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 diff --git a/anchor/README.md b/anchor/README.md index 763e289..8acc5a2 100644 --- a/anchor/README.md +++ b/anchor/README.md @@ -181,6 +181,8 @@ This matters more than the file list, so it is stated before it. | Sealing keys read from the v3 epochs, key rotation under an id (AIP-22 stage 3b, `aere.pq.registryV3.keysFrom`, `aere.falcon.keys.next`) | yes (added 2026-09-23) | **no**; proven on testnet 28001 only | | Message authorship from VERIFIED post-quantum seals, with the startup guards AERE-PQC-FIN-01/02/03 (AIP-22 stage 4, `aere.pq.finalityBlock`) | yes (added 2026-09-23) | **no**; proven on testnet 28001 only (H_fin 3,117,000) | | Checkpoint option, a header rule that refuses another hash at a checkpoint height (AIP-22 stage 4, `aere.pq.checkpoint`) | yes (added 2026-09-23) | **no**; not configured anywhere | +| Headers without ECDSA committed seals, sealed by the proposer's post-quantum seal as extraData element 6 (AIP-22 stage 5, `aere.pq.headerBlock`, guards AERE-PQC-HDR-01/02) | yes (added 2026-09-23) | **no**; proven on testnet 28001 only (H_hdr 3,131,000) | +| The hybrid (SLH-DSA) seal of a commit judged, from the v3 key height, with the key of the v3 epoch in force (a rotated row signs with its new key) | yes (corrected 2026-09-23) | **no** (no v3 epoch on chain 2800); on testnet 28001 since 2026-09-23 22:51Z | Everything not yet in force is **disarmed by absence or by height, never by a flag**: the properties that arm them are unset, and unset means never. Each refuses loudly on a value it cannot parse rather than booting diff --git a/anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java b/anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java index 9fb4a4d..d9ef6bb 100644 --- a/anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +++ b/anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java @@ -31,6 +31,7 @@ import org.hyperledger.besu.consensus.common.BftValidatorOverrides; import org.hyperledger.besu.consensus.common.EpochManager; import org.hyperledger.besu.consensus.common.ForksSchedule; import org.hyperledger.besu.consensus.common.bft.AerePqFinality; +import org.hyperledger.besu.consensus.common.bft.AerePqHeaderSeal; import org.hyperledger.besu.consensus.common.bft.AerePqIdentity; import org.hyperledger.besu.consensus.common.bft.BftBlockInterface; import org.hyperledger.besu.consensus.common.bft.BftContext; @@ -480,6 +481,15 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder { FalconSealSupport.proposalAttachBlock(), FalconSealSupport.prepareAttachBlock(), FalconSealSupport.roundChangeAttachBlock()); + // AERE AIP-22 stage 5: H_hdr (aere.pq.headerBlock), from which a header carries the proposer's post-quantum seal + // and no ECDSA committed seal; refuses unless H_fin is at or below it (AERE-PQC-HDR-01). Inert when unset. + AerePqHeaderSeal.installFromSystemProperties(); + if (AerePqHeaderSeal.headerBlock() != AerePqIdentity.NEVER) { + LOG.info( + "AERE PQ HEADER: H_hdr armed at {}; from it a header carries the proposer's post-quantum seal and NO ECDSA" + + " committed seal", + AerePqHeaderSeal.headerBlock()); + } if (AerePqFinality.finalityBlock() != AerePqIdentity.NEVER) { LOG.info( "AERE PQ FINALITY: H_fin armed at {}; from it a message's author is the id of its VERIFIED post-quantum seal and the" diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqHeaderSeal.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqHeaderSeal.java new file mode 100644 index 0000000..4f3a7c7 --- /dev/null +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqHeaderSeal.java @@ -0,0 +1,95 @@ +/* + * 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; + +/** + * AERE AIP-22 stage 5 (plan 9.7): from H_hdr ({@code aere.pq.headerBlock}) a header carries no ECDSA + * committed seal; its seventh extraData element is the proposer's Falcon-512 seal, the PROPOSAL layer's + * seal over the proposal digest, and a header is admitted between anchors iff that seal verifies for + * its {@code coinbase} (an id) at its height. Finality stays with the anchors (stage 4). H_hdr must be + * at or above H_fin, because the seal it relies on is only an AUTHOR from H_fin (AERE-PQC-HDR-01). + * Unset: never, which is every chain today. + */ +public final class AerePqHeaderSeal { + + /** The system property naming H_hdr. */ + public static final String PROPERTY = "aere.pq.headerBlock"; + + private static volatile long headerBlock = AerePqIdentity.NEVER; + + private AerePqHeaderSeal() {} + + /** + * Arms H_hdr. + * + * @param hHdr H_hdr, or {@link AerePqIdentity#NEVER} + */ + public static void install(final long hHdr) { + if (hHdr != AerePqIdentity.NEVER) { + final long hFin = AerePqFinality.finalityBlock(); + if (hFin == AerePqIdentity.NEVER || hHdr < hFin) { + throw new IllegalStateException( + "AERE-PQC-HDR-01: " + + PROPERTY + + "=" + + hHdr + + " needs H_fin (aere.pq.finalityBlock) armed at or below it (H_fin " + + (hFin == AerePqIdentity.NEVER ? "unset" : Long.toString(hFin)) + + "): the proposer's seal in the header is an author only from H_fin"); + } + } + headerBlock = hHdr; + } + + /** Disarms (tests). */ + public static void uninstall() { + headerBlock = AerePqIdentity.NEVER; + } + + /** + * H_hdr as armed. + * + * @return H_hdr, or {@link AerePqIdentity#NEVER} + */ + public static long headerBlock() { + return headerBlock; + } + + /** Arms from {@link #PROPERTY}; unset means never. Call after H_fin is installed. */ + public static void installFromSystemProperties() { + final String v = System.getProperty(PROPERTY); + if (v == null || v.isBlank()) { + uninstall(); + return; + } + final long h; + try { + h = Long.parseLong(v.trim()); + } catch (final NumberFormatException e) { + throw new IllegalStateException("AERE-PQC-HDR-01: " + PROPERTY + "='" + v + "' is not a height", e); + } + install(h); + } + + /** + * Whether a header at this height carries the proposer's seal instead of ECDSA committed seals. + * + * @param height the header's height + * @return true from H_hdr + */ + public static boolean active(final long height) { + return height >= headerBlock; + } +} diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java index daf840b..acefe61 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java @@ -117,7 +117,10 @@ public class BftBlockInterface implements BlockInterface { // AERE ANCHOR V2 (2026-09-03, D-328): the scheme-tagged certificate rides along too. Without // this line every v2 header lost its certificate at the round substitution and the fleet // refused its own first v2 anchor (testnet 28001, block 168032, 3 hours stalled). - prevExtraData.getHybridSeals()); + prevExtraData.getHybridSeals(), + // AERE AIP-22 stage 5: and the proposer's seal (element 6), for the same reason; it sits outside + // both hash pre-images, so carrying it changes no hash + prevExtraData.getPqProposerSeal()); final BlockHeaderBuilder headerBuilder = BlockHeaderBuilder.fromHeader(block.getHeader()); headerBuilder diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java index c444a15..4e9ea6f 100644 --- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java @@ -49,6 +49,12 @@ public class BftExtraData implements ParsedExtraData { private final Collection falconSeals; /** AERE ANCHOR V2: the scheme-tagged certificate; empty on every header below the v2 height. */ private final List hybridSeals; + /** + * AERE AIP-22 stage 5: the proposer's post-quantum seal (element 6 of the stored extraData, from + * H_hdr), the PROPOSAL layer's seal over the proposal digest; outside every hash, like the + * certificate. Empty on every header below H_hdr. + */ + private final Optional proposerSeal; /** * Instantiates a new Bft extra data (no Falcon seals). Retained for callers, notably the IBFT @@ -103,7 +109,33 @@ public class BftExtraData implements ParsedExtraData { final Collection
validators, final Collection falconSeals, final List hybridSeals) { + this(vanityData, seals, vote, round, validators, falconSeals, hybridSeals, Optional.empty()); + } + + /** + * Full constructor with the proposer's post-quantum seal (AIP-22 stage 5). + * + * @param vanityData the vanity data + * @param seals the ECDSA committed seals (from H_hdr: empty) + * @param vote the vote + * @param round the round + * @param validators the validators + * @param falconSeals the v1 Falcon certificate, or empty + * @param hybridSeals the v2 certificate, or empty + * @param proposerSeal the proposer's seal (element 6), or empty + */ + public BftExtraData( + final Bytes vanityData, + final Collection seals, + final Optional vote, + final int round, + final Collection
validators, + final Collection falconSeals, + final List hybridSeals, + final Optional proposerSeal) { checkNotNull(vanityData); + checkNotNull(proposerSeal); + this.proposerSeal = proposerSeal; checkNotNull(hybridSeals); if (!hybridSeals.isEmpty() && falconSeals != null && !falconSeals.isEmpty()) { throw new IllegalArgumentException( @@ -184,6 +216,30 @@ public class BftExtraData implements ParsedExtraData { return hybridSeals; } + /** + * The proposer's post-quantum seal (AIP-22 stage 5, element 6). + * + * @return the seal, or empty below H_hdr + */ + public Optional getPqProposerSeal() { + return proposerSeal; + } + + /** + * The same extra data with another proposer seal and ECDSA seals, everything else carried over + * (the certificate included): the one way to seal a stage-5 header, so no caller can drop the + * certificate while adding the seal (the lesson of 2026-09-03, D-328). + * + * @param newSeals the ECDSA committed seals (from H_hdr: empty) + * @param newProposerSeal the proposer's seal + * @return the new extra data + */ + public BftExtraData withSeals( + final Collection newSeals, final Optional newProposerSeal) { + return new BftExtraData( + vanityData, newSeals, vote, round, validators, falconSeals, hybridSeals, newProposerSeal); + } + @Override public String toString() { return "BftExtraData{" @@ -201,6 +257,8 @@ public class BftExtraData implements ParsedExtraData { + falconSeals + ", hybridSeals=" + hybridSeals + + ", proposerSeal=" + + proposerSeal + '}'; } } 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 3d0138d..9bb4499 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 @@ -22,6 +22,7 @@ package org.hyperledger.besu.consensus.qbft.core.statemachine; import static java.util.Collections.emptyList; +import org.hyperledger.besu.consensus.common.bft.AerePqHeaderSeal; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.FalconSeal; import org.hyperledger.besu.consensus.common.bft.FalconSealSupport; @@ -539,6 +540,27 @@ public class QbftRound { // least K valid seals, K being the configured schedule (6 of 9 on chain 2800 today). Between // anchor heights nothing is written. The older wording here said "a >= 2f+1 Falcon quorum // certificate" and described the legacy per-block rule, which is retired at the anchor block. + // AERE AIP-22 stage 5: from H_hdr the sealed header carries no ECDSA committed seal and the + // accepted proposal's post-quantum seal as element 6 (the same bytes on every node). A proposal + // without one cannot have been accepted from H_fin <= H_hdr; if it happens anyway nothing is + // imported, because the only other header this node could build is one every node refuses. + final long aereHeight = roundState.getRoundIdentifier().getSequenceNumber(); + if (AerePqHeaderSeal.active(aereHeight)) { + final Optional proposerSeal = roundState.getProposalFalconSeal(); + if (proposerSeal.isEmpty()) { + LOG.error( + "AERE-PQC-HDR-02: committed at {} but the accepted proposal carries no post-quantum seal;" + + " a header from H_hdr cannot be sealed without it. Not importing.", + aereHeight); + return; + } + importSealedBlock( + blockCreator.createSealedBlockWithProposerSeal( + roundState.getProposedBlock().get(), + roundState.getRoundIdentifier().getRoundNumber(), + proposerSeal.get())); + return; + } final Collection falconSeals = roundState.getFalconSeals(); final QbftBlock blockToImport = falconSeals.isEmpty() @@ -552,6 +574,11 @@ public class QbftRound { roundState.getCommitSeals(), falconSeals); + importSealedBlock(blockToImport); + } + + /** Imports a sealed block into the chain (shared by the stage-5 and the ECDSA sealing paths). */ + private void importSealedBlock(final QbftBlock blockToImport) { final long blockNumber = blockToImport.getHeader().getNumber(); if (getRoundIdentifier().getRoundNumber() > 0) { LOG.info( diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java index 2c798c9..b7ba44b 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java @@ -166,6 +166,16 @@ public class RoundState { return proposalMessage.map(p -> p.getSignedPayload().getPayload().getProposedBlock()); } + /** + * AIP-22 stage 5: the post-quantum seal of the accepted proposal, which becomes element 6 of the + * sealed header from H_hdr (every node writes the same one, the proposer's). + * + * @return the proposal's seal, or empty + */ + public Optional getProposalFalconSeal() { + return proposalMessage.flatMap(p -> p.getSignedPayload().getPayload().getFalconSeal()); + } + /** * Gets proposed block access list. * diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java index c397cab..af124c1 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java @@ -79,6 +79,22 @@ public interface QbftBlockCreator { return createSealedBlock(block, roundNumber, commitSeals); } + /** + * AIP-22 stage 5: seal a block from H_hdr, with NO ECDSA committed seal and the proposer's + * post-quantum seal as element 6, everything else of the proposed header carried over (the anchor + * certificate included). An implementation that cannot do this must say so, never seal the old way. + * + * @param block the proposed block + * @param roundNumber the round number + * @param proposerSeal the proposal's post-quantum seal + * @return the sealed block + */ + default QbftBlock createSealedBlockWithProposerSeal( + final QbftBlock block, final int roundNumber, final FalconSeal proposerSeal) { + throw new UnsupportedOperationException( + "AERE-PQC-HDR-02: this block creator cannot seal a stage-5 header (proposer seal, no ECDSA seals)"); + } + /** * Convenience empty Falcon seal collection. * diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java index 0555883..074459c 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java @@ -312,15 +312,28 @@ public final class PqCommitEnforcement { + " but the Falcon seal on the same commit is index " + falconSeal.getValidatorIndex() + " - one commit, one signer"); } - final Optional bound = hybridRegistry.address(found.getValidatorIndex()); - if (bound.isEmpty() - || !AerePqIdentity.author(Address.wrap(Bytes.wrap(bound.get())), height).equals(author)) { + // AIP-22 stage 3b: from H_k the keys and the index->author binding are those of the v3 epoch in force, as in the anchor + // producer and PqAnchorSealsRule. Paid 2026-09-23 on testnet 28001: this check alone still read the static v2 hybrid + // registry, so after the rotation at 3,097,600 the ROTATED row (index 2) signed with its new SLH-DSA key and every one of its + // commits at an anchor's parent was refused here; latent while four other validators sufficed, it stopped the chain at + // 3,135,679 when the Nethermind validator fell behind. + final boolean v3 = AerePqIdentity.keysFromV3(height); + final Optional
boundId = + v3 + ? AerePqIdentity.v3IdAt(height, found.getValidatorIndex()) + : hybridRegistry + .address(found.getValidatorIndex()) + .map(a -> AerePqIdentity.author(Address.wrap(Bytes.wrap(a)), height)); + if (boundId.isEmpty() || !boundId.get().equals(author)) { return Optional.of( "AERE HIBRID: index " + found.getValidatorIndex() - + " is not bound to the commit author " + author + " in the hybrid registry"); + + " is not bound to the commit author " + author + + (v3 ? " in the v3 epoch in force" : " in the hybrid registry")); } final Optional publicKey = - hybridRegistry.publicKey(found.getValidatorIndex(), schemeId); + v3 + ? AerePqIdentity.v3KeyAt(height, found.getValidatorIndex(), schemeId) + : hybridRegistry.publicKey(found.getValidatorIndex(), schemeId); if (publicKey.isEmpty()) { return Optional.of( "AERE HIBRID: the hybrid registry holds no " + schemeId + " key for index " diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqHybridRotationV3Test.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqHybridRotationV3Test.java new file mode 100644 index 0000000..3a3e696 --- /dev/null +++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqHybridRotationV3Test.java @@ -0,0 +1,171 @@ +/* AIP-22 stage 3b: the hybrid (SLH-DSA) seal of a COMMIT is judged, from H_k, with the key of the v3 epoch in force, not with the + * static v2 hybrid registry. + * + * Paid 2026-09-23 on testnet 28001: after the rotation at 3,097,600 the rotated row (index 2) signed its commits with its NEW + * SLH-DSA key, as the v3 epoch says, and PqCommitEnforcement still read the OLD key from the v2 hybrid registry, so every one of its + * commits at an anchor's parent was refused for six hours, invisibly while four other validators sufficed, until the chain stopped at + * 3,135,679 when the Nethermind validator fell behind. The anchor producer and PqAnchorSealsRule were already on v3. + * + * Real cryptography (Falcon-512 and SLH-DSA-SHA2-128s generated on every run, TEST keys); a v3 epoch built from the canonical row + * bytes; the v2 registry built from properties exactly as in production. The negative control plants, at run time, "keys never from + * v3" in PqCommitEnforcement: the two cases from H_k must turn red. */ +package org.hyperledger.besu.consensus.qbft.core.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.consensus.common.bft.AerePqIdentity; +import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.HybridSignerRegistry; +import org.hyperledger.besu.consensus.common.bft.PqRegistryV3; +import org.hyperledger.besu.consensus.common.bft.PqSchemeSchedule; +import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry; +import org.hyperledger.besu.consensus.common.bft.SchemeSeal; +import org.hyperledger.besu.consensus.common.bft.SealScheme; +import org.hyperledger.besu.consensus.common.bft.SealSchemes; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.crypto.SecureRandomProvider; +import org.hyperledger.besu.cryptoservices.NodeKey; +import org.hyperledger.besu.cryptoservices.NodeKeyUtils; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; + +import java.security.SecureRandom; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.TreeMap; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class PqHybridRotationV3Test { + private static final long CHAIN = 28001L; + private static final long H_ARMARE = 1_000L; + private static final long H_HIBRID = 2_000L; + private static final long H_K = 3_000L; + private static final Hash DIGEST = Hash.hash(Bytes.of(7, 7)); + private static final SealScheme FALCON = SealSchemes.FALCON_512; + private static final SealScheme SLH = SealSchemes.SLH_DSA_128S; + + private final SecureRandom random = SecureRandomProvider.createSecureRandom(); + private SealScheme.GeneratedPair falcon0; + private SealScheme.GeneratedPair slhOld; + private SealScheme.GeneratedPair slhNew; + private Address addr0; + private Address id0; + private PqCommitEnforcement enforcement; + + @BeforeEach + public void setup() { + falcon0 = FALCON.generate(random); + slhOld = SLH.generate(random); + slhNew = SLH.generate(random); + final NodeKey nk = NodeKeyUtils.generate(); + addr0 = org.hyperledger.besu.ethereum.core.Util.publicKeyToAddress(nk.getPublicKey()); + + // the v2 hybrid registry: index 0 is addr0, with the OLD SLH-DSA key (what the static registry keeps after a v3 rotation) + final Properties p = new Properties(); + p.setProperty("formatVersion", HybridSignerRegistry.FORMAT_VERSION); + p.setProperty("chainId", String.valueOf(CHAIN)); + p.setProperty("count", "1"); + p.setProperty("0.addr", addr0.toHexString()); + p.setProperty("0.key." + FALCON.id(), Bytes.wrap(falcon0.publicRegistryForm()).toHexString()); + p.setProperty("0.key." + SLH.id(), Bytes.wrap(slhOld.publicRegistryForm()).toHexString()); + final HybridSignerRegistry v2 = HybridSignerRegistry.fromProperties(p, "proba"); + + // the v3 epoch at H_k: row 0 keeps its Falcon key and carries the NEW SLH-DSA key (a rotation of the hybrid key) + final byte[] std = new byte[897]; + std[0] = 0x09; + System.arraycopy(falcon0.publicRegistryForm(), 0, std, 1, 896); + final byte[] id = PqRegistryV3.idOf(std); + id0 = Address.wrap(Bytes.wrap(id)); + final byte[] slhPub = slhNew.publicRegistryForm(); + final byte[] popF = FALCON.sign(falcon0.privateKey(), + PqRegistryV3.popMessage(CHAIN, H_K, 1, 0, id, FALCON.wireId(), std).toArrayUnsafe()).orElseThrow(); + final byte[] popS = SLH.sign(slhNew.privateKey(), + PqRegistryV3.popMessage(CHAIN, H_K, 1, 0, id, SLH.wireId(), slhPub).toArrayUnsafe()).orElseThrow(); + final TreeMap keys = new TreeMap<>(); + keys.put(FALCON.wireId() & 0xff, new PqRegistryV3.Key(FALCON, std, popF)); + keys.put(SLH.wireId() & 0xff, new PqRegistryV3.Key(SLH, slhPub, popS)); + final PqRegistryV3.Row bare = new PqRegistryV3.Row(0, id, keys, null, Map.of(), null, null); + final SECPSignature claim = nk.sign(PqRegistryV3.claimDigest(CHAIN, PqRegistryV3.rowHash(CHAIN, H_K, 1, bare))); + final PqRegistryV3.Row row = + new PqRegistryV3.Row(0, id, keys, null, Map.of(), addr0.getBytes().toArrayUnsafe(), claim.encodedBytes().toArrayUnsafe()); + final Bytes32 rh = PqRegistryV3.rowHash(CHAIN, H_K, 1, row); + final Bytes32 root = PqRegistryV3.merkleRoot(List.of(rh)); + final TreeMap epochs = new TreeMap<>(); + epochs.put(H_K, new PqRegistryV3.Registry(CHAIN, H_K, 1, List.of(row), List.of(rh), root, + PqRegistryV3.registryHash(CHAIN, H_K, 1, root))); + AerePqIdentity.uninstall(); + AerePqIdentity.install(new AerePqIdentity.Config(H_K, epochs, H_K)); + + // the Falcon part of the commit check: index 0 is addr0 below H_k and its id from H_k, the Falcon key unchanged + final PqSignerRegistry falconReg = + new PqSignerRegistry() { + @Override + public Address addressForIndexAtHistoric(final long h, final int i) { + return addressForIndexAtOwnHead(h, i); + } + + @Override + public Address addressForIndexAtOwnHead(final long h, final int i) { + return i != 0 ? null : (h >= H_K ? id0 : addr0); + } + + @Override + public boolean verifyAtHistoric(final long h, final int i, final Bytes m, final Bytes s) { + return verifyAtOwnHead(h, i, m, s); + } + + @Override + public boolean verifyAtOwnHead(final long h, final int i, final Bytes m, final Bytes s) { + return i == 0 && FALCON.verifyRaw(falcon0.publicRegistryForm(), m.toArray(), s.toArray()); + } + }; + final PqSchemeSchedule orar = + PqSchemeSchedule.parse(H_ARMARE + ":" + FALCON.id() + "," + H_HIBRID + ":" + FALCON.id() + "+" + SLH.id()); + enforcement = new PqCommitEnforcement(H_ARMARE, falconReg, orar, v2, h -> true); + } + + @AfterEach + public void disarm() { + AerePqIdentity.uninstall(); + } + + private Optional falconSeal() { + return Optional.of(new FalconSeal(0, Bytes.wrap(FALCON.sign(falcon0.privateKey(), DIGEST.getBytes().toArray()).orElseThrow()))); + } + + private List slhSeal(final SealScheme.GeneratedPair key) { + return List.of(new SchemeSeal(SLH.wireId(), 0, Bytes.wrap(SLH.sign(key.privateKey(), DIGEST.getBytes().toArray()).orElseThrow()))); + } + + @Test + public void fromHkTheRotatedSlhKeyOfTheV3EpochCounts() { + assertThat(enforcement.refusal(H_K + 5, id0, DIGEST, falconSeal(), slhSeal(slhNew))).isEmpty(); + } + + @Test + public void fromHkTheOldSlhKeyOfTheStaticRegistryNoLongerCounts() { + final Optional r = enforcement.refusal(H_K + 5, id0, DIGEST, falconSeal(), slhSeal(slhOld)); + assertThat(r).isPresent(); + assertThat(r.get()).contains("does NOT verify").contains(SLH.id()); + } + + @Test + public void belowHkTheStaticRegistryDecidesAsBefore() { + assertThat(enforcement.refusal(H_K - 5, addr0, DIGEST, falconSeal(), slhSeal(slhOld))).isEmpty(); + assertThat(enforcement.refusal(H_K - 5, addr0, DIGEST, falconSeal(), slhSeal(slhNew))).isPresent(); + } + + @Test + public void fromHkTheHybridSealCannotVouchForAnotherAuthor() { + final Optional r = + enforcement.refusal(H_K + 5, Address.fromHexString("0x" + "cd".repeat(20)), DIGEST, falconSeal(), slhSeal(slhNew)); + assertThat(r).isPresent(); + } +} diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java index d2bc042..4a2c4f9 100644 --- a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java +++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java @@ -27,13 +27,13 @@ import org.hyperledger.besu.consensus.common.bft.BftHelpers; import org.hyperledger.besu.consensus.common.bft.FalconSealSupport; import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig; import org.hyperledger.besu.consensus.common.bft.headervalidationrules.BftCoinbaseValidationRule; -import org.hyperledger.besu.consensus.common.bft.headervalidationrules.BftCommitSealsValidationRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.AereBaseFeeImportRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.FalconSealValidationRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorDigestAttachedRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorDigestRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorSealsRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.PqCheckpointRule; +import org.hyperledger.besu.consensus.qbft.headervalidationrules.PqHeaderSealRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.PqEmergencyShoutRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.PqRegistryBindingRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.QbftValidatorsValidationRule; @@ -162,7 +162,9 @@ public class QbftBlockHeaderValidationRulesetFactory { "Difficulty", BlockHeader::getDifficulty, UInt256.ONE)) .addRule(new QbftValidatorsValidationRule(useValidatorContract)) .addRule(new BftCoinbaseValidationRule()) - .addRule(new BftCommitSealsValidationRule()) + // AERE AIP-22 stage 5: upstream's committed seals rule below H_hdr, unchanged; from H_hdr the proposer's + // post-quantum seal (element 6) and an EMPTY committed-seal element (PqHeaderSealRule) + .addRule(PqHeaderSealRule.forFleet(pqAnchorConfig.chainId())) // AERE ANCORA-V2: the legacy log-only Falcon rule now stands down at H. With an // unconfigured anchor this is Long.MAX_VALUE, i.e. it never stands down and behaviour is // unchanged. diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java index b3096d3..e5f1a59 100644 --- a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java +++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java @@ -164,6 +164,20 @@ public class QbftExtraDataCodec extends BftExtraDataCodec { hybridSeals = Collections.emptyList(); } + // AERE AIP-22 stage 5: an OPTIONAL seventh element, the proposer's seal [index, signature], after + // the certificate slot. When it is present the certificate slot is ALWAYS written (an empty list + // when the header carries no certificate), so the POSITION tells the two apart, never the shape + // (a proposer seal from index 2 would otherwise look like the head of a v2 certificate), and the + // canonical round trip below keeps every value to exactly one encoding. + Optional proposerSeal = Optional.empty(); + if (!rlpInput.isEndOfCurrentList()) { + rlpInput.enterList(); + final int idx = rlpInput.readIntScalar(); + final Bytes sig = rlpInput.readBytes(); + rlpInput.leaveList(); + proposerSeal = Optional.of(new FalconSeal(idx, sig)); + } + // AERE FIX-MALEABILITATE-BLOC (2026-08-01), gate 1 of 2: the leave is STRICT. // // Upstream ends this decode with leaveListLenient(), which silently discards anything still @@ -177,7 +191,8 @@ public class QbftExtraDataCodec extends BftExtraDataCodec { rlpInput.leaveList(); final BftExtraData decoded = - new BftExtraData(vanityData, seals, vote, round, validators, falconSeals, hybridSeals); + new BftExtraData( + vanityData, seals, vote, round, validators, falconSeals, hybridSeals, proposerSeal); // AERE FIX-MALEABILITATE-BLOC (2026-08-01), gate 2 of 2: CANONICAL ROUND TRIP. // @@ -243,6 +258,16 @@ public class QbftExtraDataCodec extends BftExtraDataCodec { rlp.writeBytes(falconSeal.getSignature()); rlp.endList(); }); + } else if (bftExtraData.getPqProposerSeal().isPresent()) { + // AERE AIP-22 stage 5: the certificate slot stays in place, empty, so the seal is element 6 + encoder.writeEmptyList(); + } + if (bftExtraData.getPqProposerSeal().isPresent()) { + final FalconSeal ps = bftExtraData.getPqProposerSeal().get(); + encoder.startList(); + encoder.writeIntScalar(ps.getValidatorIndex()); + encoder.writeBytes(ps.getSignature()); + encoder.endList(); } } else { encoder.writeEmptyList(); diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java index dd67707..24769c3 100644 --- a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java +++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java @@ -87,6 +87,38 @@ public class QbftBlockCreatorAdaptor implements QbftBlockCreator { return createSealedBlock(block, roundNumber, commitSeals, Collections.emptyList()); } + /** + * AIP-22 stage 5: the stage-5 header, from H_hdr. The proposed header's extraData is carried over + * whole (vanity with the anchor digest, validators, vote, the anchor certificate verbatim) through + * {@link BftExtraData#withSeals}, so nothing the proposer chose can be dropped here; only the round + * is set to the commit round, the ECDSA committed seals are EMPTY and element 6 is the proposal's + * post-quantum seal. Every node seals the same bytes: the header is no longer assembled from what + * each node heard. + */ + @Override + public QbftBlock createSealedBlockWithProposerSeal( + final QbftBlock block, final int roundNumber, final FalconSeal proposerSeal) { + final Block besuBlock = AdaptorUtil.toBesuBlock(block); + final BftExtraData initial = bftExtraDataCodec.decode(AdaptorUtil.toBesuBlockHeader(block.getHeader())); + final BftExtraData withRound = + new BftExtraData( + initial.getVanityData(), + Collections.emptyList(), + initial.getVote(), + roundNumber, + initial.getValidators(), + initial.getFalconSeals(), + initial.getHybridSeals(), + initial.getPqProposerSeal()); + final BftExtraData sealed = withRound.withSeals(Collections.emptyList(), Optional.of(proposerSeal)); + final BlockHeader header = + BlockHeaderBuilder.fromHeader(AdaptorUtil.toBesuBlockHeader(block.getHeader())) + .extraData(bftExtraDataCodec.encode(sealed)) + .blockHeaderFunctions(BftBlockHeaderFunctions.forOnchainBlock(bftExtraDataCodec)) + .buildBlockHeader(); + return new QbftBlockAdaptor(new Block(header, besuBlock.getBody())); + } + @Override public QbftBlock createSealedBlock( final QbftBlock block, diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java new file mode 100644 index 0000000..c877c12 --- /dev/null +++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java @@ -0,0 +1,155 @@ +/* + * 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.headervalidationrules; + +import org.hyperledger.besu.consensus.common.bft.AerePqFinality; +import org.hyperledger.besu.consensus.common.bft.AerePqHeaderSeal; +import org.hyperledger.besu.consensus.common.bft.AerePqIdentity; +import org.hyperledger.besu.consensus.common.bft.BftBlockHashing; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; +import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec; +import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.PqAnchor; +import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry; +import org.hyperledger.besu.consensus.common.bft.headervalidationrules.BftCommitSealsValidationRule; +import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.mainnet.AttachedBlockHeaderValidationRule; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes32; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AIP-22 stage 5: the seal rule of a header. Below H_hdr it IS the upstream rule (a quorum of ECDSA + * committed seals of the parent's validators), unchanged. From H_hdr a header is admitted iff its ECDSA + * committed-seal element is EMPTY, its seventh element carries the proposer's Falcon-512 seal, the seal's + * index names the header's {@code coinbase} (an id) in the v3 epoch in force at its height, and the seal + * verifies over the PROPOSAL message recomputed from the stored header (chain id, height, the round in + * element 3, the proposal digest, i.e. the committed-seal hash of the header). Finality stays with the + * anchors (AIP-22 section 4); this rule is what admits a header between them. + */ +public class PqHeaderSealRule implements AttachedBlockHeaderValidationRule { + + private static final Logger LOG = LoggerFactory.getLogger(PqHeaderSealRule.class); + + private final AttachedBlockHeaderValidationRule belowHdr; + private final BftExtraDataCodec codec; + private final long chainId; + private final AerePqFinality.SealVerifier verifier; + + /** + * The rule. + * + * @param belowHdr the rule below H_hdr (upstream's committed seals rule in production) + * @param codec the extraData codec + * @param chainId the chain id every seal message carries + * @param verifier the seal verifier (the anchor rules' historic door in production) + */ + public PqHeaderSealRule( + final AttachedBlockHeaderValidationRule belowHdr, + final BftExtraDataCodec codec, + final long chainId, + final AerePqFinality.SealVerifier verifier) { + this.belowHdr = belowHdr; + this.codec = codec; + this.chainId = chainId; + this.verifier = verifier; + } + + /** + * The fleet's rule: upstream's committed seals rule below H_hdr, the verifier of the header rules above. + * + * @param chainId the chain id + * @return the rule + */ + public static PqHeaderSealRule forFleet(final long chainId) { + final PqSignerRegistry registry = PqSignerRegistry.falconSealSupport(); + return new PqHeaderSealRule( + new BftCommitSealsValidationRule(), new QbftExtraDataCodec(), chainId, registry::verifyAtHistoric); + } + + @Override + public boolean validate( + final BlockHeader header, final BlockHeader parent, final ProtocolContext protocolContext) { + if (!AerePqHeaderSeal.active(header.getNumber())) { + return belowHdr.validate(header, parent, protocolContext); + } + final Optional r = refusal(header); + r.ifPresent(LOG::info); + return r.isEmpty(); + } + + /** + * Why a header at or above H_hdr is refused, if it is. + * + * @param header the header + * @return the refusal, or empty + */ + public Optional refusal(final BlockHeader header) { + final long h = header.getNumber(); + final BftExtraData ed; + try { + ed = codec.decode(header); + } catch (final RuntimeException e) { + return Optional.of("AERE PQ HEADER: block " + h + " extraData does not decode: " + e.getMessage()); + } + if (!ed.getSeals().isEmpty()) { + return Optional.of( + "AERE PQ HEADER: block " + h + " carries " + ed.getSeals().size() + + " ECDSA committed seal(s); from H_hdr " + AerePqHeaderSeal.headerBlock() + " the element MUST be empty"); + } + if (ed.getPqProposerSeal().isEmpty()) { + return Optional.of("AERE PQ HEADER: block " + h + " carries no proposer seal (element 6) from H_hdr"); + } + final FalconSeal ps = ed.getPqProposerSeal().get(); + final Optional
id = AerePqIdentity.v3IdAt(h, ps.getValidatorIndex()); + if (id.isEmpty()) { + return Optional.of( + "AERE PQ HEADER: block " + h + " proposer seal index " + ps.getValidatorIndex() + " names no row of the v3 epoch in force"); + } + if (!id.get().equals(header.getCoinbase())) { + return Optional.of( + "AERE PQ HEADER: block " + h + " proposer seal of index " + ps.getValidatorIndex() + " (id " + id.get() + + ") but coinbase " + header.getCoinbase() + " - a seal cannot vouch for another proposer"); + } + final Bytes32 message; + try { + message = + PqAnchor.proposalMessage( + chainId, h, ed.getRound(), new BftBlockHashing(codec).calculateDataHashForCommittedSeal(header).getBytes()); + } catch (final RuntimeException e) { + return Optional.of("AERE PQ HEADER: block " + h + " proposal message cannot be built: " + e.getMessage()); + } + final boolean ok; + try { + ok = verifier.verify(h, ps.getValidatorIndex(), message, ps.getSignature()); + } catch (final RuntimeException e) { + return Optional.of("AERE PQ HEADER: block " + h + " proposer seal verification threw: " + e.getMessage()); + } + return ok + ? Optional.empty() + : Optional.of("AERE PQ HEADER: block " + h + " proposer seal of index " + ps.getValidatorIndex() + " does not verify"); + } + + @Override + public boolean includeInLightValidation() { + return belowHdr.includeInLightValidation(); + } +} diff --git a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java new file mode 100644 index 0000000..e9f3236 --- /dev/null +++ b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java @@ -0,0 +1,227 @@ +/* + * 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.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.hyperledger.besu.consensus.common.bft.AerePqFinality; +import org.hyperledger.besu.consensus.common.bft.AerePqHeaderSeal; +import org.hyperledger.besu.consensus.common.bft.AerePqIdentity; +import org.hyperledger.besu.consensus.common.bft.BftBlockHashing; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; +import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.PqAnchor; +import org.hyperledger.besu.consensus.common.bft.PqRegistryV3; +import org.hyperledger.besu.consensus.common.bft.SealScheme; +import org.hyperledger.besu.consensus.common.bft.SealSchemes; +import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.crypto.SecureRandomProvider; +import org.hyperledger.besu.cryptoservices.NodeKey; +import org.hyperledger.besu.cryptoservices.NodeKeyUtils; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.core.Util; +import org.hyperledger.besu.ethereum.mainnet.AttachedBlockHeaderValidationRule; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * AIP-22 stage 5 in the Besu-derived client, with real Falcon-512 keys generated for the run: the + * seventh extraData element (the proposer's seal) round-trips canonically, stays outside BOTH hashes, + * and the header rule admits a header from H_hdr iff its ECDSA committed seals are empty and the seal + * of its coinbase's index verifies over the proposal message recomputed from the stored header. Each + * refusal is asserted BY ITS REASON. Below H_hdr the upstream rule decides. The runner's negative + * control plants "never refuse" in the rule at run time and these probes turn red. + */ +public class PqHeaderSealStage5Test { + + private static final long CHAIN = 28001; + private static final long BIND = 10; + private static final long H_ID = 10; + private static final long H_FIN = 20; + private static final long H_HDR = 30; + private static final int N = 4; + private static final SealScheme FALCON = SealSchemes.FALCON_512; + private static final QbftExtraDataCodec CODEC = new QbftExtraDataCodec(); + + private final List falcon = new ArrayList<>(); + private final List
ids = new ArrayList<>(); + private PqHeaderSealRule rule; + + @BeforeEach + public void arm() { + final List rows = new ArrayList<>(); + for (int i = 0; i < N; i++) { + final NodeKey nk = NodeKeyUtils.generate(); + final SealScheme.GeneratedPair p = FALCON.generate(SecureRandomProvider.createSecureRandom()); + falcon.add(p.privateKey()); + final byte[] std = new byte[897]; + std[0] = 0x09; + System.arraycopy(p.publicRegistryForm(), 0, std, 1, 896); + final byte[] id = PqRegistryV3.idOf(std); + final Bytes32 popMsg = PqRegistryV3.popMessage(CHAIN, BIND, N, i, id, FALCON.wireId(), std); + final byte[] pop = FALCON.sign(p.privateKey(), popMsg.toArrayUnsafe()).orElseThrow(); + final TreeMap keys = new TreeMap<>(); + keys.put(FALCON.wireId() & 0xff, new PqRegistryV3.Key(FALCON, std, pop)); + final PqRegistryV3.Row bare = new PqRegistryV3.Row(i, id, keys, null, Map.of(), null, null); + final SECPSignature claim = nk.sign(PqRegistryV3.claimDigest(CHAIN, PqRegistryV3.rowHash(CHAIN, BIND, N, bare))); + rows.add( + new PqRegistryV3.Row( + i, id, keys, null, Map.of(), + Util.publicKeyToAddress(nk.getPublicKey()).getBytes().toArrayUnsafe(), + claim.encodedBytes().toArrayUnsafe())); + ids.add(Address.wrap(Bytes.wrap(id))); + } + final List hashes = new ArrayList<>(); + for (final PqRegistryV3.Row r : rows) { + hashes.add(PqRegistryV3.rowHash(CHAIN, BIND, N, r)); + } + final Bytes32 root = PqRegistryV3.merkleRoot(hashes); + final TreeMap epochs = new TreeMap<>(); + epochs.put( + BIND, + new PqRegistryV3.Registry(CHAIN, BIND, N, rows, hashes, root, PqRegistryV3.registryHash(CHAIN, BIND, N, root))); + AerePqHeaderSeal.uninstall(); + AerePqFinality.uninstall(); + AerePqIdentity.uninstall(); + AerePqIdentity.install(new AerePqIdentity.Config(H_ID, epochs, H_ID)); + final AerePqFinality.SealVerifier v = + (h, i, m, s) -> + AerePqIdentity.v3KeyAt(h, i, "falcon-512") + .map(k -> FALCON.verifyRaw(k, m.toArrayUnsafe(), s.toArrayUnsafe())) + .orElse(false); + AerePqFinality.install(H_FIN, CHAIN, v); + AerePqHeaderSeal.install(H_HDR); + rule = new PqHeaderSealRule(sentinel(false), CODEC, CHAIN, v); + } + + @AfterEach + public void disarm() { + AerePqHeaderSeal.uninstall(); + AerePqFinality.uninstall(); + AerePqIdentity.uninstall(); + } + + private static AttachedBlockHeaderValidationRule sentinel(final boolean answer) { + return (header, parent, ctx) -> answer; + } + + private BftExtraData extra(final int round, final List seals, final Optional ps) { + return new BftExtraData( + Bytes.wrap(new byte[32]), seals, Optional.empty(), round, ids, Collections.emptyList(), Collections.emptyList(), ps); + } + + private static BlockHeader header(final long h, final Address coinbase, final BftExtraData ed) { + return new BlockHeaderTestFixture().number(h).coinbase(coinbase).extraData(CODEC.encode(ed)).buildHeader(); + } + + /** The proposer seal of {@code signer} over the header's own proposal message (for the index {@code index}). */ + private Optional sealOver(final BlockHeader unsealed, final int round, final int index, final int signer) { + final Bytes32 m = + PqAnchor.proposalMessage( + CHAIN, unsealed.getNumber(), round, new BftBlockHashing(CODEC).calculateDataHashForCommittedSeal(unsealed).getBytes()); + return Optional.of(new FalconSeal(index, Bytes.wrap(FALCON.sign(falcon.get(signer), m.toArrayUnsafe()).orElseThrow()))); + } + + private BlockHeader sealedHeader(final long h, final int round, final int coinbase, final int index, final int signer) { + final BlockHeader unsealed = header(h, ids.get(coinbase), extra(round, List.of(), Optional.empty())); + return header(h, ids.get(coinbase), extra(round, List.of(), sealOver(unsealed, round, index, signer))); + } + + @Test + public void theProposerSealIsTheSeventhElementCanonicalAndOutsideBothHashes() { + final BlockHeader unsealed = header(H_HDR, ids.get(1), extra(2, List.of(), Optional.empty())); + final BlockHeader sealed = sealedHeader(H_HDR, 2, 1, 1, 1); + final BftExtraData back = CODEC.decode(sealed); + assertThat(back.getPqProposerSeal()).isPresent(); + assertThat(back.getPqProposerSeal().get().getValidatorIndex()).isEqualTo(1); + assertThat(back.getSeals()).isEmpty(); + assertThat(back.getRound()).isEqualTo(2); + final BftBlockHashing hashing = new BftBlockHashing(CODEC); + assertThat(hashing.calculateHashOfBftBlockOnchain(sealed)).isEqualTo(hashing.calculateHashOfBftBlockOnchain(unsealed)); + assertThat(hashing.calculateDataHashForCommittedSeal(sealed)).isEqualTo(hashing.calculateDataHashForCommittedSeal(unsealed)); + assertThat(sealed.getExtraData().size()).isGreaterThan(unsealed.getExtraData().size() + 600); + } + + @Test + public void anEmptyCertificateSlotWithoutTheProposerSealIsNonCanonicalAndRefused() { + // six elements whose sixth is an explicit empty list: two encodings of one value, refused as before + final BytesValueRLPOutput o = new BytesValueRLPOutput(); + o.startList(); + o.writeBytes(Bytes.wrap(new byte[32])); + o.writeList(ids, (a, rlp) -> rlp.writeBytes(a.getBytes())); + o.writeEmptyList(); + o.writeIntScalar(0); + o.writeEmptyList(); + o.writeEmptyList(); + o.endList(); + assertThatThrownBy(() -> CODEC.decodeRaw(o.encoded())).hasMessageContaining("Non-canonical"); + } + + @Test + public void fromHhdrAHeaderWithAVerifiedProposerSealOfItsCoinbaseAndNoEcdsaSealIsAdmitted() { + for (int i = 0; i < N; i++) { + assertThat(rule.refusal(sealedHeader(H_HDR, 0, i, i, i))).as("proposer %s", i).isEmpty(); + } + assertThat(rule.validate(sealedHeader(H_HDR + 7, 3, 2, 2, 2), null, null)).isTrue(); + } + + @Test + public void fromHhdrEachWayOfBeingWrongIsRefusedByItsReason() { + // an ECDSA committed seal present + final SECPSignature ecdsa = NodeKeyUtils.generate().sign(Bytes32.ZERO); + final BlockHeader unsealed = header(H_HDR, ids.get(0), extra(0, List.of(), Optional.empty())); + final BlockHeader withEcdsa = header(H_HDR, ids.get(0), extra(0, List.of(ecdsa), sealOver(unsealed, 0, 0, 0))); + assertThat(rule.refusal(withEcdsa)).get().asString().contains("MUST be empty"); + // no proposer seal + assertThat(rule.refusal(unsealed)).get().asString().contains("no proposer seal"); + // index 0 sealed by key 1 + assertThat(rule.refusal(sealedHeader(H_HDR, 0, 0, 0, 1))).get().asString().contains("does not verify"); + // a valid seal of index 1 on a header whose coinbase is id 0 + assertThat(rule.refusal(sealedHeader(H_HDR, 0, 0, 1, 1))).get().asString().contains("cannot vouch"); + // an index with no row + assertThat(rule.refusal(sealedHeader(H_HDR, 0, 0, N, 0))).get().asString().contains("names no row"); + // a seal over another round than the header's + final BlockHeader r1 = header(H_HDR, ids.get(0), extra(1, List.of(), Optional.empty())); + final BlockHeader sealedAtRound0 = header(H_HDR, ids.get(0), extra(1, List.of(), sealOver(unsealed, 0, 0, 0))); + assertThat(rule.refusal(sealedAtRound0)).get().asString().contains("does not verify"); + assertThat(rule.refusal(header(H_HDR, ids.get(0), extra(1, List.of(), sealOver(r1, 1, 0, 0))))).isEmpty(); + } + + @Test + public void belowHhdrTheUpstreamRuleDecidesAndHhdrNeedsHfin() { + final BlockHeader below = sealedHeader(H_HDR - 1, 0, 0, 0, 0); + assertThat(new PqHeaderSealRule(sentinel(false), CODEC, CHAIN, (h, i, m, s) -> true).validate(below, null, null)).isFalse(); + assertThat(new PqHeaderSealRule(sentinel(true), CODEC, CHAIN, (h, i, m, s) -> false).validate(below, null, null)).isTrue(); + assertThatThrownBy(() -> AerePqHeaderSeal.install(H_FIN - 1)).hasMessageStartingWith("AERE-PQC-HDR-01"); + AerePqFinality.uninstall(); + assertThatThrownBy(() -> AerePqHeaderSeal.install(H_HDR)).hasMessageStartingWith("AERE-PQC-HDR-01"); + } +}