From 71412b979268dc5fbb3d10169f8ebd8fafdacc24 Mon Sep 17 00:00:00 2001 From: Aere Network Date: Wed, 23 Sep 2026 22:48:38 +0300 Subject: [PATCH] anchor: the corpus carries AIP-22 stage 4 (message authorship from VERIFIED post-quantum seals, the startup guards AERE-PQC-FIN-01/02/03, the checkpoint option), unarmed on chain 2800 and proven on testnet 28001 (H_fin 3,117,000); the armed-on-2800 table says so row by row; LICENSING.md lists the new files --- LICENSING.md | 13 +- anchor/MANIFEST-sha256.txt | 21 +- anchor/README.md | 2 + .../controller/QbftBesuControllerBuilder.java | 24 ++ .../consensus/common/bft/AerePqFinality.java | 194 ++++++++++++++ .../besu/consensus/common/bft/PqSealed.java | 61 +++++ .../common/bft/AerePqFinalityAuthorTest.java | 243 ++++++++++++++++++ .../qbft/core/payload/CommitPayload.java | 16 +- .../qbft/core/payload/PreparePayload.java | 17 +- .../qbft/core/payload/ProposalPayload.java | 17 +- .../qbft/core/payload/RoundChangePayload.java | 22 +- .../validation/PqFinalityLayersGuard.java | 181 +++++++++++++ .../validation/AereFinalityMessageTest.java | 204 +++++++++++++++ .../validation/AereFinalityVectorsWriter.java | 206 +++++++++++++++ .../validation/PqFinalityLayersGuardTest.java | 130 ++++++++++ ...ftBlockHeaderValidationRulesetFactory.java | 7 +- .../PqCheckpointRule.java | 133 ++++++++++ .../PqCheckpointRuleTest.java | 75 ++++++ 18 files changed, 1553 insertions(+), 13 deletions(-) create mode 100644 anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java create mode 100644 anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealed.java create mode 100644 anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java create mode 100644 anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuard.java create mode 100644 anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityMessageTest.java create mode 100644 anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityVectorsWriter.java create mode 100644 anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuardTest.java create mode 100644 anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRule.java create mode 100644 anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRuleTest.java diff --git a/LICENSING.md b/LICENSING.md index cf7ef5f..7375c52 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -132,8 +132,8 @@ repository, and the publisher refuses to ship a package whose list disagrees wit You do not need that tool to check the list: every row below names a file in this package, and `grep -i copyright` on it shows the same line. -Measured 2026-09-23 over `anchor/`: **189 files**. **90** carry someone -else's copyright alone, **45** carry a shared notice naming both upstream and us, and **11** name only +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. 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. @@ -149,6 +149,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar | `anchor/app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `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/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 | @@ -165,6 +166,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | +| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealed.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | @@ -191,6 +193,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java` | `Copyright contributors to Besu.` | upstream | +| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java` | `Copyright contributors to Besu / Aere Network.` | upstream, **modified by Aere Network** | @@ -228,6 +231,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar | `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java` | `Copyright 2020 ConsenSys AG.` | upstream | | `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/MessageValidator.java` | `Copyright ConsenSys AG.` | upstream | +| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuard.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcement.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqProposalEnforcement.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqRoundChangeEnforcement.java` | `Copyright contributors to Besu.` | upstream | @@ -241,6 +245,9 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayloadPqTest.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqHybridExtrasNeverThrowTest.java` | `Copyright contributors to Hyperledger Besu.` | upstream | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqLateSealSalvageTest.java` | `Copyright contributors to Besu.` | upstream | +| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityMessageTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | +| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityVectorsWriter.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | +| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuardTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqMessageAgilityTest.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareAgilityTest.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcementTest.java` | `Copyright contributors to Besu.` | upstream | @@ -259,6 +266,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/PqAnchorDigestAttachedRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `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/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** | @@ -268,6 +276,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/PqAnchorSealsRuleTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqArmedWithoutRegistryTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | +| `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/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java` | `Copyright ConsenSys AG.` | upstream | diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt index 5cfa14f..a86cc5c 100644 --- a/anchor/MANIFEST-sha256.txt +++ b/anchor/MANIFEST-sha256.txt @@ -5,11 +5,12 @@ 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 -6a324fd14b4dea445ac08a7ef43bedb800c64c11c4bcbaefb58f98ec9051e60a app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +6bc5a9ae4dbeda9a26e3157cb61c938cc306c859ee597526107199e0c88166ae 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 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 @@ -34,6 +35,7 @@ f67a205036550f7d0f1bfed2abe7b89abd73e80614adff8d696b33c580269a6f consensus/comm 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 73abbaddcec1533a7a758f1c4a26ae68a83ddbb96fd98ad147e74d7386bbbf3f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.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 @@ -67,6 +69,7 @@ ed1cc6f617dc5a2f7801b6e8f6f039a035a2ffc9542b116262dac39bb70563dd consensus/comm 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 +27e5782c1742a361db7a22269ac44c1af574e25b07a87c76c10792bc9741b65c consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java 15d2d2cfc627406ae6183353a81703fd40da871b216ff8e9b1c4d15af2437805 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqIdentityTest.java acd5c1e7f49dbbf444c19c91386bbd03b9298d952d2d13721793670103e71ee9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D177NeutralNamesTest.java c574ec22e642ac464b1728d27e4bb26202448a45c044f08935cc0a0efa0297d9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FalconAttachIntervalTest.java @@ -111,11 +114,11 @@ ae37d3525697bf84defd796bb8813193c78f81e7fa24a72f8f048285d52399e2 consensus/comm 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 597d96d64c9394c79d5084a1ab5df884b2266efe315c0f2d563bfc17b7421480 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java -03173f2029f767e10cc978b5576b9557efa783dab2ef105aa856f6d5e08b9352 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java +35d38accf0d22b6fdadeab4f2bce0cdac708f8115591594b1bb1fa385b227c5c consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java d365bad33f3aee4597c630071ca1f727ddd8bb6ab6515fccbce0a07842037adf consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java -e5c34515bebb3e53d790c2ca5c4f195db0ca022ec2603c28d47ac171cb74f2cc consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayload.java -efb0e336e19a16ef192e3d1f91f1e5488f632e17b5748654e75008ddfcdda932 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java -02d31e71cc50b1a2330c7a928c833d8b4b5f969228f085d54d099eb5d29eb65f consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java +7914cc3f4a8a85dc7e2e8dad1ea9add86c4df00b04c2f7d822d4fed60a3aadcb consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayload.java +8f8b041e72cfaee43d2f4e61a790e603ed35b329cf431de68fc6907a18c3b824 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java +e19b12e6db33cfb9bb1c2f22e321815130dfbcc5730bb505320f08a45a371093 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java 51eddf3cb8e37489abd35e6f4c36294a13ec76337bbbbfd54c07584085dceaac consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java 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 @@ -124,6 +127,7 @@ efb0e336e19a16ef192e3d1f91f1e5488f632e17b5748654e75008ddfcdda932 consensus/qbft 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 +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 afb487f8c1dd87e323fdc1153ecfdf8ec6cb5a10e0726783d4214c07a3fa9a95 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqRoundChangeEnforcement.java @@ -139,12 +143,15 @@ df5d7d2119cf0864a8c2dce50f4f69bd8b64804cfda7f7366c013b304af0a6e7 consensus/qbft 97ac1e46ab37238a2b6232eccdd4636ac6fa928c8e3d4f8a473c083c5d5c3b39 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqHybridExtrasNeverThrowTest.java 0bb2c1e31a7f29e9b07fb7030567746fc6d79460ed4739d5ecafac44501f7bb5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqLateSealSalvageTest.java 127de5fbe8014768a657f31bc77d7e66557273febdf3412a5536e8628fd70c5c consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqSealsWithoutProposalTest.java +c1b31f5c357a1966cdd1e963369a1eb3078a381679c6adc8fb5206f754eca84b consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityMessageTest.java +70bc4d5242e5f5560333c862bef0b2d5167649aabb3cd66fe61a101678d30b4d consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityVectorsWriter.java 11c968ba3eb4adabb548af5a0f6192b21ec75ff01431cc678fdfc6c13f0d4937 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereIdentityMessageTest.java 3f945a0a73288e42c82026ead5d118ab6b7a8200aacc881855aff442d0954c1c consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidatorAnchorFormTest.java 8c40173c7b248edec7c29ee9e049f9e269f4725b637bceb8734735fc73eb9ca2 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidatorPqEnforcementTest.java 3374a7ee317a807528c79b8218dbb34b308de8b7a908cb6680b83356641fcade consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/MessageValidatorAnchorFormPlumbingTest.java 401c25f63abb248dfe60b69676a34ea255bb669b4bc58e45916f6eecea3cb870 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcementTest.java 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 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 @@ -156,7 +163,7 @@ 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 -79dea5e85b968af696bf57d51d0de175a4c025537200402e3594a4a8290d1a29 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.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 cfb0aad408ab620f28b09914ece7d8d3d09f9e6bbb503070e28d9ef39c023dee consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/blockcreation/QbftBlockCreatorFactory.java @@ -165,6 +172,7 @@ e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft ab0fcd8722dcb76560f0ef8fda8af9c2b8b6ec9b468326aaf616c853526f7f81 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestAttachedRule.java 63313f944fcda0db45c40d4df840a465e6147cc85997b8ca832f0cfefea97987 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java 0f348ac4fa519f27f52f631dd290eab6e84980739a09ec767c8ca69e4a4f8f64 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java +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 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 @@ -178,6 +186,7 @@ a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft 535cca4f1083c8a25bd29877e988b99704666853c1fe8b0ba32d71efe4e15f37 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java 2a76e2500e7d26fc2443f4a37211d59eb1e80e736430dca0785dd2f3f6ea1daf consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorV2RulesTest.java 321cb7e6923f77e078b523316650ac49136cf8d4df506b9bb235a32795c69611 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqArmedWithoutRegistryTest.java +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 7ea99f24bc1c9131e8ef08f7a2c6f2a08af71ebee011a4cb88a367bd446b6c27 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java diff --git a/anchor/README.md b/anchor/README.md index 5b8f129..763e289 100644 --- a/anchor/README.md +++ b/anchor/README.md @@ -179,6 +179,8 @@ This matters more than the file list, so it is stated before it. | Per-scheme anchor intervals, Falcon-512 every 32 and SLH-DSA every 128 (AIP-22 stage 2, `scheme/interval` steps in `aere.pq.schemeSchedule`) | yes (added 2026-09-23) | **no**; proven on testnet 28001 only | | Post-quantum validator identity and the v3 key registry (AIP-22 stage 3, `aere.pq.identityBlock`, `aere.pq.registryV3`) | yes (added 2026-09-23) | **no**; proven on testnet 28001 only | | 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 | 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 e85efbf..9fb4a4d 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 @@ -30,6 +30,7 @@ import org.hyperledger.besu.config.QbftFork; 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.AerePqIdentity; import org.hyperledger.besu.consensus.common.bft.BftBlockInterface; import org.hyperledger.besu.consensus.common.bft.BftContext; @@ -89,6 +90,7 @@ 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.types.QbftValidatorProvider; import org.hyperledger.besu.consensus.qbft.core.validation.MessageValidatorFactory; +import org.hyperledger.besu.consensus.qbft.core.validation.PqFinalityLayersGuard; import org.hyperledger.besu.consensus.qbft.jsonrpc.QbftJsonRpcMethods; import org.hyperledger.besu.consensus.qbft.network.QbftGossiperImpl; import org.hyperledger.besu.consensus.qbft.protocol.Istanbul100SubProtocol; @@ -463,6 +465,28 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder { .verifyLocalKeysCoverV3EpochsOrAbort(Util.publicKeyToAddress(nodeKey.getPublicKey())); HybridSealSupport.instance() .verifyLocalKeysCoverV3EpochsOrAbort(Util.publicKeyToAddress(nodeKey.getPublicKey())); + // AERE AIP-22 stage 4: H_fin (aere.pq.finalityBlock), from which a message's author is the id of its post-quantum seal's + // index once that seal verifies, and the envelope's ECDSA is not read; refuses unless the keys are read from the v3 epochs + // at or below it (AERE-PQC-FIN-01). Inert when unset, which is every chain today. + AerePqFinality.installFromSystemProperties(aereAnchorConfig.chainId()); + // ...and the four seal layers must be armed at or below it (AERE-PQC-FIN-02): the commit layer is the one that judges a + // COMMIT's seal (its author is bound to the commit seal's signer), and the layers stay the second judge of the rest. + PqFinalityLayersGuard.verifyOrAbort(AerePqFinality.finalityBlock()); + // ...and a node that seals must seal all four messages from H_fin, or it would be mute there (AERE-PQC-FIN-03) + PqFinalityLayersGuard.verifyEmissionOrAbort( + AerePqFinality.finalityBlock(), + FalconSealSupport.instance().signingEnabled(), + FalconSealSupport.instance().attachBlock(), + FalconSealSupport.proposalAttachBlock(), + FalconSealSupport.prepareAttachBlock(), + FalconSealSupport.roundChangeAttachBlock()); + 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" + + " envelope's ECDSA is not read (four seal layers armed at or below it, this node seals: {})", + AerePqFinality.finalityBlock(), + FalconSealSupport.instance().signingEnabled()); + } PqAnchorSyncModeGuard.verifySyncModeOrAbort( syncConfig == null || syncConfig.getSyncMode() == null diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java new file mode 100644 index 0000000..64dec0d --- /dev/null +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java @@ -0,0 +1,194 @@ +/* + * 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.consensus.common.bft.payload.Payload; +import org.hyperledger.besu.datatypes.Address; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** + * AERE AIP-22 stage 4 (plan 9.6.8): from H_fin ({@code aere.pq.finalityBlock}) the ECDSA signature of a + * consensus message's envelope is no longer a criterion. The author of a message is the post-quantum id + * of the row its seal's index names in the v3 epoch in force at the message's height, and ONLY once that + * seal VERIFIES: a PROPOSAL, PREPARE or ROUND-CHANGE seal over its own domain, through the same verifier + * the seal layers use; a COMMIT's index only when it names the id of the commit seal's signer (the commit + * layer, armed at or below H_fin by AERE-PQC-FIN-02, judges the seal itself). The envelope's recovered + * ECDSA address is not read for authorship, so a validator may sign the envelope with any secp256k1 key. + * Anything else at or above H_fin has no author ({@link #NO_AUTHOR}, never in a set). + * + *

WHY HERE AND NOT ONLY IN THE LAYERS (read in the code 2026-09-23 ~18:02Z): QbftController relays a message + * as soon as its author is in the set, BEFORE the layers judge it. An author read from an unverified + * index would let anyone make every validator relay garbage in any validator's name; before H_fin that + * needed a validator's ECDSA key. The layers keep judging exactly as before. + * + *

Requires the post-quantum identity (stage 3) and the keys read from the v3 epochs (stage 3b): + * H_fin must be at or above H_k, else startup is refused (AERE-PQC-FIN-01). Unset: never, which is + * every chain today. + */ +public final class AerePqFinality { + + /** The system property naming H_fin. */ + public static final String PROPERTY = "aere.pq.finalityBlock"; + + /** The author of a message that cannot have one from H_fin (no seal, or an index with no row). */ + public static final Address NO_AUTHOR = Address.ZERO; + + /** Verifies a Falcon seal of a registry index at a height (the seal layers' verifier in production). */ + @FunctionalInterface + public interface SealVerifier { + /** + * Whether the seal verifies. + * + * @param height the message's height + * @param index the seal's registry index + * @param message the message the seal signs + * @param signature the seal's signature + * @return true when it verifies under that index's key + */ + boolean verify(long height, int index, Bytes message, Bytes signature); + } + + private static volatile long finalityBlock = AerePqIdentity.NEVER; + private static volatile long chainId; + private static volatile SealVerifier verifier = (h, i, m, s) -> false; + + private AerePqFinality() {} + + /** + * Arms H_fin (tests and the startup path). + * + * @param hFin H_fin, or {@link AerePqIdentity#NEVER} + * @param chain the chain id every seal message carries + * @param sealVerifier the verifier of the seal layers + */ + public static void install(final long hFin, final long chain, final SealVerifier sealVerifier) { + if (hFin != AerePqIdentity.NEVER) { + final long keysFrom = + AerePqIdentity.installed().map(AerePqIdentity.Config::keysFrom).orElse(AerePqIdentity.NEVER); + if (keysFrom == AerePqIdentity.NEVER || hFin < keysFrom) { + throw new IllegalStateException( + "AERE-PQC-FIN-01: " + + PROPERTY + + "=" + + hFin + + " needs the post-quantum identity and the keys read from the v3 epochs (aere.pq.registryV3.keysFrom)" + + " at or below it (keysFrom " + + (keysFrom == AerePqIdentity.NEVER ? "unset" : Long.toString(keysFrom)) + + ")"); + } + if (chain <= 0 || sealVerifier == null) { + throw new IllegalStateException( + "AERE-PQC-FIN-01: " + + PROPERTY + + "=" + + hFin + + " needs the chain id (aere.pq.chainId) and a seal verifier: every author it gives is a verified seal"); + } + } + finalityBlock = hFin; + chainId = chain; + verifier = sealVerifier == null ? (h, i, m, s) -> false : sealVerifier; + } + + /** + * H_fin as armed on this node. + * + * @return H_fin, or {@link AerePqIdentity#NEVER} when unset + */ + public static long finalityBlock() { + return finalityBlock; + } + + /** Disarms (tests). */ + public static void uninstall() { + finalityBlock = AerePqIdentity.NEVER; + chainId = 0; + verifier = (h, i, m, s) -> false; + } + + /** + * Arms from {@link #PROPERTY}; unset means never. Call after the identity is installed. + * + * @param chain the chain id of the anchor configuration (aere.pq.chainId) + */ + public static void installFromSystemProperties(final long chain) { + 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-FIN-01: " + PROPERTY + "='" + v + "' is not a height", e); + } + final PqSignerRegistry layers = PqSignerRegistry.falconSealSupport(); + install(h, chain, layers::verifyAtOwnHead); + } + + /** + * Whether the post-quantum seal is the author of messages at this height. + * + * @param height the message's height + * @return true from H_fin when armed + */ + public static boolean active(final long height) { + return height >= finalityBlock; + } + + /** + * The author of a message from H_fin: the id of its seal's index at the message's height, once the + * seal verifies (a COMMIT: once the index names the commit seal signer's id). + * + * @param payload the message payload + * @param height the message's height + * @return the id, or {@link #NO_AUTHOR} + */ + public static Address authorFromSeal(final Payload payload, final long height) { + if (!(payload instanceof PqSealed sealed)) { + return NO_AUTHOR; + } + final Optional seal = sealed.getFalconSeal(); + if (seal.isEmpty()) { + return NO_AUTHOR; + } + final int index = seal.get().getValidatorIndex(); + final Optional

id = AerePqIdentity.v3IdAt(height, index); + if (id.isEmpty()) { + return NO_AUTHOR; + } + try { + final Optional
commitSigner = sealed.commitSealSigner(); + if (commitSigner.isPresent()) { + return id.get().equals(AerePqIdentity.author(commitSigner.get(), height)) ? id.get() : NO_AUTHOR; + } + final Optional message = sealed.pqSealMessage(chainId); + if (message.isEmpty()) { + return NO_AUTHOR; + } + return verifier.verify(height, index, message.get(), seal.get().getSignature()) + ? id.get() + : NO_AUTHOR; + } catch (final RuntimeException e) { + // a seal we cannot judge authors nothing: "cannot judge" is never a pass + return NO_AUTHOR; + } + } +} diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealed.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealed.java new file mode 100644 index 0000000..34049da --- /dev/null +++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealed.java @@ -0,0 +1,61 @@ +/* + * 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.datatypes.Address; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes32; + +/** + * AERE AIP-22 stage 4: a consensus payload that carries a post-quantum seal {@code (index, signature)}. + * The four QBFT payloads (proposal, prepare, commit, round-change) implement it; from H_fin the author + * of a message is the id of its seal's index, and only once the seal is VERIFIED + * ({@link AerePqFinality#authorFromSeal}): the author decides whether a message is even relayed, which + * happens before the layers judge it. + */ +public interface PqSealed { + + /** + * The post-quantum seal this payload carries, if any. + * + * @return the seal + */ + Optional getFalconSeal(); + + /** + * The message this payload's seal signs over a chain, as the seal layer that judges it builds it + * (PREPARE, PROPOSAL and ROUND-CHANGE carry every field of it). Empty when the payload alone does not + * determine it (a COMMIT's seal signs the block's on-chain hash). + * + * @param chainId the chain id that goes into the message + * @return the message, or empty + */ + default Optional pqSealMessage(final long chainId) { + return Optional.empty(); + } + + /** + * For a COMMIT, the address that signed its commit seal; empty for every other payload. From H_fin a + * COMMIT's author is its seal index's id only when that is this signer's id (the binding its seal + * layer checks), and the seal itself is judged by the commit layer, armed at or below H_fin. + * + * @return the commit seal's signer, or empty + */ + default Optional
commitSealSigner() { + return Optional.empty(); + } +} diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java new file mode 100644 index 0000000..4b875f9 --- /dev/null +++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java @@ -0,0 +1,243 @@ +/* + * 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.assertThatThrownBy; + +import org.hyperledger.besu.consensus.common.bft.payload.Payload; +import org.hyperledger.besu.consensus.common.bft.payload.SignedData; +import org.hyperledger.besu.crypto.Hash; +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.ethereum.core.Util; +import org.hyperledger.besu.ethereum.rlp.RLPOutput; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.TreeMap; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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 4 (iii): from H_fin the author of a message is the id of its post-quantum seal's index, + * ONCE THE SEAL VERIFIES, and the ECDSA signature of the envelope is not read. Every message here is + * signed by a FOREIGN key (not any validator's), so a validator's id can only come from the seal. The + * verifier is injected with the seal layers' contract (height, index, message, signature): it accepts + * exactly the signature {@code keccak(index || message)}, so a seal of another index, over another + * message, or of any other bytes does not verify. A COMMIT's author is its index's id only when that is + * the commit seal signer's id. Below H_fin, and with H_fin unset, the author is the (translated) ECDSA + * signer, as in stage 3. The runner's negative controls plant "H_fin never active" and "every seal + * verifies" at run time and these probes turn red. + */ +public class AerePqFinalityAuthorTest { + + private static final SignatureAlgorithm ALG = SignatureAlgorithmFactory.getInstance(); + private static final long CHAIN = 28001L; + private static final AerePqFinality.SealVerifier V = + (h, i, m, s) -> s.equals(sigFor(i, m)); + + private long hId; + private final List
id = new ArrayList<>(); + private final List
ecdsa = new ArrayList<>(); + private final KeyPair foreign = ALG.generateKeyPair(); + + private static Bytes sigFor(final int index, final Bytes message) { + // the index as its 32 bits (a negative index has a signature too, it just names no row) + return Hash.keccak256(Bytes.concatenate(Bytes.ofUnsignedLong(Integer.toUnsignedLong(index)), message)); + } + + private static Bytes32 messageAt(final long height) { + return Hash.keccak256(Bytes.concatenate(Bytes.wrap("t4-msg".getBytes(java.nio.charset.StandardCharsets.US_ASCII)), Bytes.ofUnsignedLong(height))); + } + + /** A PREPARE-like payload: its seal signs {@link #messageAt}(height); a COMMIT-like one names a commit seal signer. */ + private static final class Sealed implements Payload, PqSealed { + private final long height; + private final Optional seal; + private final Optional
commitSigner; + private final boolean messageThrows; + + Sealed(final long height, final Optional seal, final Optional
commitSigner, final boolean messageThrows) { + this.height = height; + this.seal = seal; + this.commitSigner = commitSigner; + this.messageThrows = messageThrows; + } + + @Override + public void writeTo(final RLPOutput out) { + out.startList(); + out.writeLongScalar(height); + out.endList(); + } + + @Override + public int getMessageType() { + return 0x12; + } + + @Override + public org.hyperledger.besu.datatypes.Hash hashForSignature() { + return org.hyperledger.besu.datatypes.Hash.hash(Bytes.ofUnsignedLong(height)); + } + + @Override + public ConsensusRoundIdentifier getRoundIdentifier() { + return new ConsensusRoundIdentifier(height, 0); + } + + @Override + public Optional getFalconSeal() { + return seal; + } + + @Override + public Optional pqSealMessage(final long chainId) { + if (messageThrows) { + throw new IllegalArgumentException("cannot build"); + } + return commitSigner.isPresent() || chainId != CHAIN ? Optional.empty() : Optional.of(messageAt(height)); + } + + @Override + public Optional
commitSealSigner() { + return commitSigner; + } + } + + private Address signed(final Sealed p) { + return SignedData.create(p, ALG.sign(Bytes32.wrap(p.hashForSignature().getBytes()), foreign)).getAuthor(); + } + + private static Sealed prepare(final long height, final Optional seal) { + return new Sealed(height, seal, Optional.empty(), false); + } + + /** A seal of index {@code index} that verifies over the message of {@code signedHeight}. */ + private static Optional good(final int index, final long signedHeight) { + return Optional.of(new FalconSeal(index, sigFor(index, messageAt(signedHeight)))); + } + + @BeforeEach + public void arm() throws IOException { + final JsonNode v; + try (InputStream in = getClass().getResourceAsStream("/aere/registry-v3/ok-epoch-a.json")) { + assertThat(in).isNotNull(); + v = new ObjectMapper().readTree(in); + } + final PqRegistryV3.Registry epoch = + PqRegistryV3.load( + v.get("manifest"), v.get("chainId").asLong(), + List.of("falcon-512", "slh-dsa-sha2-128s"), null, true); + for (final PqRegistryV3.Row r : epoch.rows()) { + id.add(Address.wrap(Bytes.wrap(r.id()))); + ecdsa.add(Address.wrap(Bytes.wrap(r.ecdsaAddress()))); + } + hId = epoch.bindHeight(); + final TreeMap epochs = new TreeMap<>(); + epochs.put(hId, epoch); + AerePqFinality.uninstall(); + AerePqIdentity.uninstall(); + AerePqIdentity.install(new AerePqIdentity.Config(hId, epochs, hId)); + } + + @AfterEach + public void disarm() { + AerePqFinality.uninstall(); + AerePqIdentity.uninstall(); + } + + @Test + public void fromHfinTheAuthorIsTheIdOfAVerifiedSealWhateverKeySignedTheEnvelope() { + final long hFin = hId + 100; + AerePqFinality.install(hFin, CHAIN, V); + final Address foreignAddr = Util.publicKeyToAddress(foreign.getPublicKey()); + assertThat(id).doesNotContain(foreignAddr); + for (int i = 0; i < id.size(); i++) { + assertThat(signed(prepare(hFin, good(i, hFin)))).as("index %s at H_fin", i).isEqualTo(id.get(i)); + assertThat(signed(prepare(hFin + 500, good(i, hFin + 500)))).isEqualTo(id.get(i)); + } + // one block below H_fin the envelope's signer is still the author: a foreign key authors nothing of the set + assertThat(signed(prepare(hFin - 1, good(0, hFin - 1)))).isEqualTo(foreignAddr); + } + + @Test + public void fromHfinASealThatDoesNotVerifyAuthorsNothing() { + final long hFin = hId + 100; + AerePqFinality.install(hFin, CHAIN, V); + // the fixture can express the defect: the same index with its own seal IS an author + assertThat(signed(prepare(hFin, good(0, hFin)))).isEqualTo(id.get(0)); + // index 0 carrying index 1's seal + assertThat(signed(prepare(hFin, Optional.of(new FalconSeal(0, sigFor(1, messageAt(hFin))))))) + .isEqualTo(AerePqFinality.NO_AUTHOR); + // index 0's seal over another height's message + assertThat(signed(prepare(hFin, good(0, hFin + 1)))).isEqualTo(AerePqFinality.NO_AUTHOR); + // any other bytes + assertThat(signed(prepare(hFin, Optional.of(new FalconSeal(0, Bytes.of(1, 2, 3)))))) + .isEqualTo(AerePqFinality.NO_AUTHOR); + // no seal, an index outside the epoch, a negative index, a message that cannot be built + assertThat(signed(prepare(hFin, Optional.empty()))).isEqualTo(AerePqFinality.NO_AUTHOR); + assertThat(signed(prepare(hFin, good(id.size(), hFin)))).isEqualTo(AerePqFinality.NO_AUTHOR); + assertThat(signed(prepare(hFin, good(-1, hFin)))).isEqualTo(AerePqFinality.NO_AUTHOR); + assertThat(signed(new Sealed(hFin, good(0, hFin), Optional.empty(), true))) + .isEqualTo(AerePqFinality.NO_AUTHOR); + } + + @Test + public void fromHfinACommitsAuthorIsItsIndexOnlyWhenThatIsTheCommitSealSigner() { + final long hFin = hId + 100; + AerePqFinality.install(hFin, CHAIN, V); + final Optional anySeal = Optional.of(new FalconSeal(0, Bytes.of(9))); + assertThat(signed(new Sealed(hFin, anySeal, Optional.of(ecdsa.get(0)), false))).isEqualTo(id.get(0)); + assertThat(signed(new Sealed(hFin, anySeal, Optional.of(ecdsa.get(1)), false))) + .isEqualTo(AerePqFinality.NO_AUTHOR); + assertThat( + signed( + new Sealed( + hFin, anySeal, Optional.of(Util.publicKeyToAddress(foreign.getPublicKey())), false))) + .isEqualTo(AerePqFinality.NO_AUTHOR); + assertThat(signed(new Sealed(hFin, Optional.empty(), Optional.of(ecdsa.get(0)), false))) + .isEqualTo(AerePqFinality.NO_AUTHOR); + } + + @Test + public void unsetHfinLeavesTheEcdsaSignerAsTheAuthorAndHfinNeedsKeysFromV3AndAChain() { + final Address foreignAddr = Util.publicKeyToAddress(foreign.getPublicKey()); + assertThat(signed(prepare(hId + 100, good(0, hId + 100)))).isEqualTo(foreignAddr); + // H_fin below H_k, without a chain id, or without keys read from the v3 epochs refuses to arm + assertThatThrownBy(() -> AerePqFinality.install(hId - 1, CHAIN, V)) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("AERE-PQC-FIN-01"); + assertThatThrownBy(() -> AerePqFinality.install(hId + 100, 0, V)) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("AERE-PQC-FIN-01"); + AerePqIdentity.uninstall(); + assertThatThrownBy(() -> AerePqFinality.install(hId + 100, CHAIN, V)) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("AERE-PQC-FIN-01"); + } +} diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java index 8b788f4..cdfaaf2 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java @@ -22,6 +22,7 @@ package org.hyperledger.besu.consensus.qbft.core.payload; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.PqSealed; import org.hyperledger.besu.consensus.common.bft.PqAnchorV2; import org.hyperledger.besu.consensus.common.bft.SchemeSeal; import org.hyperledger.besu.consensus.common.bft.SealSchemes; @@ -29,7 +30,9 @@ import org.hyperledger.besu.consensus.common.bft.payload.Payload; import org.hyperledger.besu.consensus.qbft.core.messagedata.QbftV1; 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.RLPException; import org.hyperledger.besu.ethereum.rlp.RLPInput; import org.hyperledger.besu.ethereum.rlp.RLPOutput; @@ -54,7 +57,7 @@ import org.apache.tuweni.bytes.Bytes; *

AERE FIX-MALEABILITATE: this payload has exactly ONE valid encoding for a given value. See * {@link #readFrom(RLPInput)}. */ -public class CommitPayload extends QbftPayload { +public class CommitPayload extends QbftPayload implements PqSealed { private static final int TYPE = QbftV1.COMMIT; private final ConsensusRoundIdentifier roundIdentifier; private final Hash digest; @@ -323,10 +326,21 @@ public class CommitPayload extends QbftPayload { * * @return the Falcon seal if this commit carried one, otherwise empty */ + @Override public Optional getFalconSeal() { return falconSeal; } + /** AIP-22 stage 4: the commit seal's signer, recovered as CommitValidator recovers it (empty when it does not recover). */ + @Override + public Optional

commitSealSigner() { + try { + return Optional.ofNullable(Util.signatureToAddress(commitSeal, digest)); + } catch (final RuntimeException e) { + return Optional.empty(); + } + } + /** * Gets the non-Falcon scheme seals of a hybrid certificate. * diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayload.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayload.java index 727cbcc..dea9b70 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayload.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayload.java @@ -22,6 +22,8 @@ package org.hyperledger.besu.consensus.qbft.core.payload; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.PqAnchor; +import org.hyperledger.besu.consensus.common.bft.PqSealed; import org.hyperledger.besu.consensus.common.bft.payload.Payload; import org.hyperledger.besu.consensus.qbft.core.messagedata.QbftV1; import org.hyperledger.besu.datatypes.Hash; @@ -34,6 +36,7 @@ import java.util.Optional; import java.util.StringJoiner; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; /** * The Prepare payload. @@ -54,7 +57,7 @@ import org.apache.tuweni.bytes.Bytes; * bytes as a commit seal, a PREPARE seal given honestly could be pasted onto a forged COMMIT and * the enforcement there would accept it. */ -public class PreparePayload extends QbftPayload { +public class PreparePayload extends QbftPayload implements PqSealed { private static final int TYPE = QbftV1.PREPARE; private final ConsensusRoundIdentifier roundIdentifier; private final Hash digest; @@ -171,10 +174,22 @@ public class PreparePayload extends QbftPayload { * * @return the seal, or empty */ + @Override public Optional getFalconSeal() { return falconSeal; } + /** AIP-22 stage 4: the PREPARE seal's message, as PqPrepareEnforcement builds it. */ + @Override + public Optional pqSealMessage(final long chainId) { + return Optional.of( + PqAnchor.prepareMessage( + chainId, + roundIdentifier.getSequenceNumber(), + roundIdentifier.getRoundNumber(), + digest.getBytes())); + } + @Override public ConsensusRoundIdentifier getRoundIdentifier() { return roundIdentifier; diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java index 813e798..ada90e3 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java @@ -22,6 +22,8 @@ package org.hyperledger.besu.consensus.qbft.core.payload; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.PqAnchor; +import org.hyperledger.besu.consensus.common.bft.PqSealed; import org.hyperledger.besu.consensus.qbft.core.messagedata.QbftV1; import org.hyperledger.besu.consensus.qbft.core.types.QbftBlock; import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec; @@ -35,6 +37,7 @@ import java.util.Objects; import java.util.Optional; import com.google.common.base.MoreObjects; +import org.apache.tuweni.bytes.Bytes32; /** * The Proposal payload. @@ -63,7 +66,7 @@ import com.google.common.base.MoreObjects; * and it must be the last element; otherwise the message is refused. Messages of the upstream * shapes (three or four elements) are read exactly as upstream reads them. */ -public class ProposalPayload extends QbftPayload { +public class ProposalPayload extends QbftPayload implements PqSealed { private static final int TYPE = QbftV1.PROPOSAL; private final ConsensusRoundIdentifier roundIdentifier; @@ -202,10 +205,22 @@ public class ProposalPayload extends QbftPayload { * * @return the seal, or empty */ + @Override public Optional getFalconSeal() { return falconSeal; } + /** AIP-22 stage 4: the PROPOSAL seal's message over the proposed block's hash, as PqProposalEnforcement builds it. */ + @Override + public Optional pqSealMessage(final long chainId) { + return Optional.of( + PqAnchor.proposalMessage( + chainId, + roundIdentifier.getSequenceNumber(), + roundIdentifier.getRoundNumber(), + proposedBlock.getHash().getBytes())); + } + @Override public int getMessageType() { return TYPE; diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java index 8804b6a..736b9e7 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java @@ -22,6 +22,8 @@ package org.hyperledger.besu.consensus.qbft.core.payload; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.PqAnchor; +import org.hyperledger.besu.consensus.common.bft.PqSealed; import org.hyperledger.besu.consensus.qbft.core.messagedata.QbftV1; import org.hyperledger.besu.ethereum.rlp.RLPException; import org.hyperledger.besu.ethereum.rlp.RLPInput; @@ -31,6 +33,7 @@ import java.util.Objects; import java.util.Optional; import com.google.common.base.MoreObjects; +import org.apache.tuweni.bytes.Bytes32; /** * The Round change payload. @@ -61,7 +64,7 @@ import com.google.common.base.MoreObjects; * it must be the last element; otherwise the message is refused. Messages of the upstream shape * (three elements) are read exactly as upstream reads them. */ -public class RoundChangePayload extends QbftPayload { +public class RoundChangePayload extends QbftPayload implements PqSealed { private static final int TYPE = QbftV1.ROUND_CHANGE; private final ConsensusRoundIdentifier roundChangeIdentifier; private final Optional preparedRoundMetadata; @@ -115,10 +118,27 @@ public class RoundChangePayload extends QbftPayload { * * @return the seal, or empty */ + @Override public Optional getFalconSeal() { return falconSeal; } + /** AIP-22 stage 4: the ROUND-CHANGE seal's message, prepared or not, as PqRoundChangeEnforcement builds it. */ + @Override + public Optional pqSealMessage(final long chainId) { + final long height = roundChangeIdentifier.getSequenceNumber(); + final int targetRound = roundChangeIdentifier.getRoundNumber(); + return Optional.of( + preparedRoundMetadata.isPresent() + ? PqAnchor.roundChangeMessage( + chainId, + height, + targetRound, + preparedRoundMetadata.get().getPreparedRound(), + preparedRoundMetadata.get().getPreparedBlockHash().getBytes()) + : PqAnchor.roundChangeMessage(chainId, height, targetRound)); + } + @Override public void writeTo(final RLPOutput rlpOutput) { // RLP encode of the message data content (round identifier and prepared certificate) diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuard.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuard.java new file mode 100644 index 0000000..ea85af1 --- /dev/null +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuard.java @@ -0,0 +1,181 @@ +/* + * 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.core.validation; + +import org.hyperledger.besu.consensus.common.bft.AerePqIdentity; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.LongPredicate; + +/** + * AIP-22 stage 4: H_fin may be armed only where every post-quantum seal is VERIFIED from H_fin. + * + *

From H_fin a message's author is the id of its seal's index ({@code AerePqFinality}), and the + * envelope's ECDSA signature is no longer read. For a PROPOSAL, PREPARE or ROUND-CHANGE the author is + * given only once the seal verifies; a COMMIT's author is its index's id only when that is the commit + * seal signer's id, and what judges the COMMIT's post-quantum seal is the commit layer alone. Each + * layer is armed on its own height. With the commit layer unset, or armed above H_fin, a Commit whose + * ECDSA commit seal was forged (the adversary stage 4 exists for) would count with any bytes as its + * post-quantum seal; and the other three layers are the second judge of what the author already + * checked. So this guard refuses to start such a node (AERE-PQC-FIN-02), before the network is up. + * Emergency disarming of a layer after H_fin therefore needs H_fin unset too, on every node: one + * uniform step back to ECDSA authorship, never a node that trusts unverified seals. + */ +public final class PqFinalityLayersGuard { + + /** + * One seal layer as the guard sees it. + * + * @param property the property that arms it + * @param armedAt whether it is armed at a height; null when the property is set nowhere + */ + public record Layer(String property, LongPredicate armedAt) {} + + private PqFinalityLayersGuard() {} + + /** + * The refusal, if any. + * + * @param hFin H_fin, or {@link AerePqIdentity#NEVER} + * @param layers the four seal layers + * @return empty when H_fin is unset or every layer is armed at or below it; otherwise the refusal + */ + public static Optional refusal(final long hFin, final List layers) { + if (hFin == AerePqIdentity.NEVER) { + return Optional.empty(); + } + final List missing = new ArrayList<>(); + for (final Layer l : layers) { + if (l.armedAt() == null || !l.armedAt().test(hFin)) { + missing.add(l.property() + (l.armedAt() == null ? " unset" : " armed above " + hFin)); + } + } + if (missing.isEmpty()) { + return Optional.empty(); + } + return Optional.of( + "AERE-PQC-FIN-02: aere.pq.finalityBlock=" + + hFin + + " makes a message's author the id of its post-quantum seal's index, so every seal" + + " must be verified from H_fin; not armed at or below it: " + + String.join(", ", missing) + + ". Arm the four layers at or below H_fin on every node, or unset H_fin on every node."); + } + + /** + * The four layers as this node's configuration arms them. + * + * @return proposal, prepare, commit, round-change + */ + public static List fromSystemConfig() { + final PqProposalEnforcement p = PqProposalEnforcement.fromSystemConfig(); + final PqPrepareEnforcement pr = PqPrepareEnforcement.fromSystemConfig(); + final PqCommitEnforcement c = PqCommitEnforcement.fromSystemConfig(); + final PqRoundChangeEnforcement rc = PqRoundChangeEnforcement.fromSystemConfig(); + return List.of( + new Layer(PqProposalEnforcement.PROPERTY_FORK_BLOCK, p == null ? null : p::armedAt), + new Layer(PqPrepareEnforcement.PROPERTY_FORK_BLOCK, pr == null ? null : pr::armedAt), + new Layer(PqCommitEnforcement.PROPERTY_FORK_BLOCK, c == null ? null : c::armedAt), + new Layer( + PqRoundChangeEnforcement.PROPERTY_FORK_BLOCK, rc == null ? null : rc::armedAt)); + } + + /** + * Refuses to start when H_fin is armed over an unverified layer. + * + * @param hFin H_fin as armed on this node + * @throws IllegalStateException AERE-PQC-FIN-02 + */ + public static void verifyOrAbort(final long hFin) { + final Optional r = refusal(hFin, fromSystemConfig()); + if (r.isPresent()) { + throw new IllegalStateException(r.get()); + } + } + + /** + * AERE-PQC-FIN-03: from H_fin an unsealed message has no author, so a node that seals (holds its + * Falcon key; from H_k every registered validator must, stage 3b-ii) must seal all four messages at + * or below H_fin, or it goes mute at H_fin while looking healthy: a loss of liveness, not safety. + * + * @param hFin H_fin, or {@link AerePqIdentity#NEVER} + * @param signs whether this node seals (a reader does not) + * @param attachCommit aere.falcon.attachBlock ({@link Long#MAX_VALUE} when unset) + * @param attachProposal aere.pq.proposalPq.attachBlock + * @param attachPrepare aere.pq.preparePq.attachBlock + * @param attachRoundChange aere.pq.roundChangePq.attachBlock + * @return the refusal, or empty + */ + public static Optional emissionRefusal( + final long hFin, + final boolean signs, + final long attachCommit, + final long attachProposal, + final long attachPrepare, + final long attachRoundChange) { + if (hFin == AerePqIdentity.NEVER || !signs) { + return Optional.empty(); + } + final List missing = new ArrayList<>(); + gate(missing, "aere.falcon.attachBlock", attachCommit, hFin); + gate(missing, "aere.pq.proposalPq.attachBlock", attachProposal, hFin); + gate(missing, "aere.pq.preparePq.attachBlock", attachPrepare, hFin); + gate(missing, "aere.pq.roundChangePq.attachBlock", attachRoundChange, hFin); + if (missing.isEmpty()) { + return Optional.empty(); + } + return Optional.of( + "AERE-PQC-FIN-03: aere.pq.finalityBlock=" + + hFin + + " makes an unsealed message authorless, and this node would not seal from H_fin: " + + String.join(", ", missing) + + ". It would go mute at H_fin. Arm them at or below H_fin, or unset H_fin, on every validator."); + } + + private static void gate(final List missing, final String property, final long h, final long hFin) { + if (h < 0 || h == Long.MAX_VALUE) { + missing.add(property + " unset"); + } else if (h > hFin) { + missing.add(property + " armed above H_fin (" + h + ")"); + } + } + + /** + * Refuses to start when a sealing node would not seal every message from H_fin. + * + * @param hFin H_fin as armed on this node + * @param signs whether this node seals + * @param attachCommit commit attachment height + * @param attachProposal proposal attachment height + * @param attachPrepare prepare attachment height + * @param attachRoundChange round-change attachment height + * @throws IllegalStateException AERE-PQC-FIN-03 + */ + public static void verifyEmissionOrAbort( + final long hFin, + final boolean signs, + final long attachCommit, + final long attachProposal, + final long attachPrepare, + final long attachRoundChange) { + final Optional r = + emissionRefusal(hFin, signs, attachCommit, attachProposal, attachPrepare, attachRoundChange); + if (r.isPresent()) { + throw new IllegalStateException(r.get()); + } + } +} diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityMessageTest.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityMessageTest.java new file mode 100644 index 0000000..e8df550 --- /dev/null +++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityMessageTest.java @@ -0,0 +1,204 @@ +/* + * 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.core.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.consensus.common.bft.AerePqFinality; +import org.hyperledger.besu.consensus.common.bft.AerePqIdentity; +import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; +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.common.bft.payload.Payload; +import org.hyperledger.besu.consensus.common.bft.payload.SignedData; +import org.hyperledger.besu.consensus.qbft.core.QbftBlockTestFixture; +import org.hyperledger.besu.consensus.qbft.core.payload.CommitPayload; +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.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.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.util.ArrayList; +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; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * AIP-22 stage 4 on the REAL QBFT payloads, with real Falcon-512 keys generated for the run: from H_fin + * the author of a PREPARE, a PROPOSAL and a ROUND-CHANGE is the id of its seal's index only when the + * seal verifies over the message its own layer builds, and a COMMIT's only when that index names the + * commit seal signer's id. Every envelope is signed by a FOREIGN node key, so a validator's id can only + * come from the seal. Each positive has its negative on the same fixture: another index's seal, a seal + * over another round, a ROUND-CHANGE with prepared metadata sealed in the unprepared form, a proposal + * sealed over another block, a commit seal of another validator. The runner's negative control plants + * "every seal verifies" at run time and these probes turn red. + */ +@ExtendWith(MockitoExtension.class) +public class AereFinalityMessageTest { + + private static final long CHAIN = 28001; + private static final long BIND = 10; // H_k must be the bindHeight of a v3 epoch at or above H_id + private static final long H_ID = 10; + private static final long H_FIN = 20; + private static final int N = 4; + private static final SealScheme FALCON = SealSchemes.FALCON_512; + + private final Hash digest = Hash.hash(Bytes.of(7)); + private final NodeKey foreign = NodeKeyUtils.generate(); + private final List ecdsa = new ArrayList<>(); + private final List falcon = new ArrayList<>(); + private final List

ids = new ArrayList<>(); + private @Mock QbftBlockCodec blockCodec; + + @BeforeEach + public void registry() { + final List rows = new ArrayList<>(); + for (int i = 0; i < N; i++) { + final NodeKey nk = NodeKeyUtils.generate(); + ecdsa.add(nk); + 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 Bytes32 rh = PqRegistryV3.rowHash(CHAIN, BIND, N, bare); + final SECPSignature claim = nk.sign(PqRegistryV3.claimDigest(CHAIN, rh)); + final Address addr = org.hyperledger.besu.ethereum.core.Util.publicKeyToAddress(nk.getPublicKey()); + rows.add( + new PqRegistryV3.Row( + i, id, keys, null, Map.of(), addr.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 PqRegistryV3.Registry reg = + new PqRegistryV3.Registry( + CHAIN, BIND, N, rows, hashes, root, PqRegistryV3.registryHash(CHAIN, BIND, N, root)); + final TreeMap epochs = new TreeMap<>(); + epochs.put(BIND, reg); + AerePqFinality.uninstall(); + AerePqIdentity.uninstall(); + AerePqIdentity.install(new AerePqIdentity.Config(H_ID, epochs, H_ID)); + // the production verifier reads the same v3 key from H_k (FalconSealSupport.verifyAtOwnHead) + AerePqFinality.install( + H_FIN, + CHAIN, + (h, i, m, s) -> + AerePqIdentity.v3KeyAt(h, i, "falcon-512") + .map(k -> FALCON.verifyRaw(k, m.toArrayUnsafe(), s.toArrayUnsafe())) + .orElse(false)); + } + + @AfterEach + public void disarm() { + AerePqFinality.uninstall(); + AerePqIdentity.uninstall(); + } + + private Optional seal(final int index, final int signer, final Bytes32 message) { + return Optional.of( + new FalconSeal(index, Bytes.wrap(FALCON.sign(falcon.get(signer), message.toArrayUnsafe()).orElseThrow()))); + } + + private

Address authorOf(final P payload) { + return SignedData.create(payload, foreign.sign(Bytes32.wrap(payload.hashForSignature().getBytes()))) + .getAuthor(); + } + + private static ConsensusRoundIdentifier round(final int r) { + return new ConsensusRoundIdentifier(H_FIN, r); + } + + @Test + public void aPrepareIsAuthoredOnlyByAVerifiedSealOfItsIndex() { + final Bytes32 m = PqAnchor.prepareMessage(CHAIN, H_FIN, 0, digest.getBytes()); + for (int i = 0; i < N; i++) { + assertThat(authorOf(new PreparePayload(round(0), digest, seal(i, i, m)))).as("node %s", i).isEqualTo(ids.get(i)); + } + // index 0 with node 1's seal; node 0's seal over round 1; no seal + assertThat(authorOf(new PreparePayload(round(0), digest, seal(0, 1, m)))).isEqualTo(AerePqFinality.NO_AUTHOR); + assertThat( + authorOf( + new PreparePayload( + round(0), digest, seal(0, 0, PqAnchor.prepareMessage(CHAIN, H_FIN, 1, digest.getBytes()))))) + .isEqualTo(AerePqFinality.NO_AUTHOR); + assertThat(authorOf(new PreparePayload(round(0), digest, Optional.empty()))).isEqualTo(AerePqFinality.NO_AUTHOR); + } + + @Test + public void aRoundChangeIsAuthoredOnlyByASealInTheFormItsMetadataNames() { + final Bytes32 bare = PqAnchor.roundChangeMessage(CHAIN, H_FIN, 2); + assertThat(authorOf(new RoundChangePayload(round(2), Optional.empty(), seal(1, 1, bare)))).isEqualTo(ids.get(1)); + final Optional meta = Optional.of(new PreparedRoundMetadata(digest, 1)); + final Bytes32 prepared = PqAnchor.roundChangeMessage(CHAIN, H_FIN, 2, 1, digest.getBytes()); + assertThat(authorOf(new RoundChangePayload(round(2), meta, seal(1, 1, prepared)))).isEqualTo(ids.get(1)); + // the prepared claim sealed in the unprepared form does not author: the seal must cover the claim + assertThat(authorOf(new RoundChangePayload(round(2), meta, seal(1, 1, bare)))).isEqualTo(AerePqFinality.NO_AUTHOR); + } + + @Test + public void aProposalIsAuthoredOnlyByASealOverItsOwnBlock() { + final QbftBlock block = + new QbftBlockTestFixture() + .blockHeader(new QbftBlockHeaderTestFixture().number(H_FIN).buildHeader()) + .build(); + final Bytes32 m = PqAnchor.proposalMessage(CHAIN, H_FIN, 0, block.getHash().getBytes()); + assertThat(authorOf(new ProposalPayload(round(0), block, blockCodec, Optional.empty(), seal(2, 2, m)))) + .isEqualTo(ids.get(2)); + final Bytes32 other = PqAnchor.proposalMessage(CHAIN, H_FIN, 0, digest.getBytes()); + assertThat(authorOf(new ProposalPayload(round(0), block, blockCodec, Optional.empty(), seal(2, 2, other)))) + .isEqualTo(AerePqFinality.NO_AUTHOR); + } + + @Test + public void aCommitIsAuthoredByItsIndexOnlyWhenThatIsTheCommitSealSigner() { + final Optional any = Optional.of(new FalconSeal(3, Bytes.of(1))); + final SECPSignature own = ecdsa.get(3).sign(Bytes32.wrap(digest.getBytes())); + final SECPSignature other = ecdsa.get(0).sign(Bytes32.wrap(digest.getBytes())); + assertThat(authorOf(new CommitPayload(round(0), digest, own, any))).isEqualTo(ids.get(3)); + assertThat(authorOf(new CommitPayload(round(0), digest, other, any))).isEqualTo(AerePqFinality.NO_AUTHOR); + } +} diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityVectorsWriter.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityVectorsWriter.java new file mode 100644 index 0000000..b1a6776 --- /dev/null +++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityVectorsWriter.java @@ -0,0 +1,206 @@ +/* + * 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.core.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.consensus.common.bft.AerePqFinality; +import org.hyperledger.besu.consensus.common.bft.AerePqIdentity; +import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; +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.core.messagewrappers.Commit; +import org.hyperledger.besu.consensus.qbft.core.messagewrappers.Prepare; +import org.hyperledger.besu.consensus.qbft.core.messagewrappers.RoundChange; +import org.hyperledger.besu.consensus.qbft.core.payload.MessageFactory; +import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec; +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 org.hyperledger.besu.ethereum.core.Util; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +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.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.mockito.Mockito; + +/** + * AIP-22 stage 4: the WRITER of the shared authorship vectors. Real QBFT messages, encoded on the wire + * exactly as this client sends them, each ENVELOPE signed by a FOREIGN node key, each carrying a real + * Falcon-512 seal (keys generated for the run, nothing here touches a real key): the second client must + * decode every one to the SAME author this client gives it. Every expected author is asserted here, at + * writing time, through this client's own decoding path (SignedData), so a vector cannot say what this + * client does not do. Runs only with AERE_T4_VECTORS_OUT set to the output directory. + */ +@EnabledIfEnvironmentVariable(named = "AERE_T4_VECTORS_OUT", matches = ".+") +public class AereFinalityVectorsWriter { + + 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 int N = 4; + private static final SealScheme FALCON = SealSchemes.FALCON_512; + + @AfterEach + public void disarm() { + AerePqFinality.uninstall(); + AerePqIdentity.uninstall(); + } + + private static String hex(final Bytes b) { + return b.toUnprefixedHexString(); + } + + @Test + public void writeTheSharedAuthorshipVectors() throws IOException { + final List ecdsa = new ArrayList<>(); + final List falcon = new ArrayList<>(); + final List std = new ArrayList<>(); + final List

ids = new ArrayList<>(); + final List rows = new ArrayList<>(); + for (int i = 0; i < N; i++) { + final NodeKey nk = NodeKeyUtils.generate(); + ecdsa.add(nk); + final SealScheme.GeneratedPair p = FALCON.generate(SecureRandomProvider.createSecureRandom()); + falcon.add(p.privateKey()); + final byte[] s = new byte[897]; + s[0] = 0x09; + System.arraycopy(p.publicRegistryForm(), 0, s, 1, 896); + std.add(s); + final byte[] id = PqRegistryV3.idOf(s); + final Bytes32 popMsg = PqRegistryV3.popMessage(CHAIN, BIND, N, i, id, FALCON.wireId(), s); + final byte[] pop = FALCON.sign(p.privateKey(), popMsg.toArrayUnsafe()).orElseThrow(); + final TreeMap keys = new TreeMap<>(); + keys.put(FALCON.wireId() & 0xff, new PqRegistryV3.Key(FALCON, s, 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))); + AerePqIdentity.install(new AerePqIdentity.Config(H_ID, epochs, H_ID)); + AerePqFinality.install( + H_FIN, + CHAIN, + (h, i, m, s) -> + AerePqIdentity.v3KeyAt(h, i, "falcon-512") + .map(k -> FALCON.verifyRaw(k, m.toArrayUnsafe(), s.toArrayUnsafe())) + .orElse(false)); + + final NodeKey foreign = NodeKeyUtils.generate(); + final Address foreignAddr = Util.publicKeyToAddress(foreign.getPublicKey()); + final MessageFactory mf = new MessageFactory(foreign, Mockito.mock(QbftBlockCodec.class)); + final Hash digest = Hash.hash(Bytes.of(7)); + final List out = new ArrayList<>(); // name, type, height, wire, expected author + + // 1. a PREPARE sealed by its own index: the index's id + final Bytes32 pm = PqAnchor.prepareMessage(CHAIN, H_FIN, 0, digest.getBytes()); + final Prepare p1 = mf.createPrepare(new ConsensusRoundIdentifier(H_FIN, 0), digest, seal(falcon, 1, 1, pm)); + assertThat(p1.getAuthor()).isEqualTo(ids.get(1)); + out.add(new String[] {"prepare-own-seal", "prepare", Long.toString(H_FIN), hex(p1.encode()), hex(ids.get(1).getBytes())}); + // 2. a PREPARE naming index 0 with index 1's seal: no author + final Prepare p2 = mf.createPrepare(new ConsensusRoundIdentifier(H_FIN, 0), digest, seal(falcon, 0, 1, pm)); + assertThat(p2.getAuthor()).isEqualTo(AerePqFinality.NO_AUTHOR); + out.add(new String[] {"prepare-another-index-seal", "prepare", Long.toString(H_FIN), hex(p2.encode()), hex(AerePqFinality.NO_AUTHOR.getBytes())}); + // 3. a PREPARE one block below H_fin: the envelope's (foreign) signer + final Bytes32 pmb = PqAnchor.prepareMessage(CHAIN, H_FIN - 1, 0, digest.getBytes()); + final Prepare p3 = mf.createPrepare(new ConsensusRoundIdentifier(H_FIN - 1, 0), digest, seal(falcon, 1, 1, pmb)); + assertThat(p3.getAuthor()).isEqualTo(foreignAddr); + out.add(new String[] {"prepare-below-hfin", "prepare", Long.toString(H_FIN - 1), hex(p3.encode()), hex(foreignAddr.getBytes())}); + // 4. a ROUND-CHANGE without a prepared claim, sealed by its own index + final Bytes32 rm = PqAnchor.roundChangeMessage(CHAIN, H_FIN, 2); + final RoundChange r1 = mf.createRoundChange(new ConsensusRoundIdentifier(H_FIN, 2), Optional.empty(), seal(falcon, 2, 2, rm)); + assertThat(r1.getAuthor()).isEqualTo(ids.get(2)); + out.add(new String[] {"roundchange-own-seal", "roundchange", Long.toString(H_FIN), hex(r1.encode()), hex(ids.get(2).getBytes())}); + // 5. a ROUND-CHANGE sealed over another target round: no author + final RoundChange r2 = + mf.createRoundChange( + new ConsensusRoundIdentifier(H_FIN, 2), Optional.empty(), + seal(falcon, 2, 2, PqAnchor.roundChangeMessage(CHAIN, H_FIN, 3))); + assertThat(r2.getAuthor()).isEqualTo(AerePqFinality.NO_AUTHOR); + out.add(new String[] {"roundchange-other-round", "roundchange", Long.toString(H_FIN), hex(r2.encode()), hex(AerePqFinality.NO_AUTHOR.getBytes())}); + // 6. a COMMIT naming index 3 with validator 3's commit seal: index 3's id + final SECPSignature own = ecdsa.get(3).sign(Bytes32.wrap(digest.getBytes())); + final Commit c1 = mf.createCommit(new ConsensusRoundIdentifier(H_FIN, 0), digest, own, Optional.of(new FalconSeal(3, Bytes.of(1)))); + assertThat(c1.getAuthor()).isEqualTo(ids.get(3)); + out.add(new String[] {"commit-own-commit-seal", "commit", Long.toString(H_FIN), hex(c1.encode()), hex(ids.get(3).getBytes())}); + // 7. a COMMIT naming index 3 with validator 0's commit seal: no author + final SECPSignature other = ecdsa.get(0).sign(Bytes32.wrap(digest.getBytes())); + final Commit c2 = mf.createCommit(new ConsensusRoundIdentifier(H_FIN, 0), digest, other, Optional.of(new FalconSeal(3, Bytes.of(1)))); + assertThat(c2.getAuthor()).isEqualTo(AerePqFinality.NO_AUTHOR); + out.add(new String[] {"commit-another-commit-seal", "commit", Long.toString(H_FIN), hex(c2.encode()), hex(AerePqFinality.NO_AUTHOR.getBytes())}); + + final StringBuilder j = new StringBuilder(); + j.append("{\n \"note\": \"AIP-22 stage 4 shared authorship vectors, written by AereFinalityVectorsWriter (Besu-derived client); PROBE keys\",\n"); + j.append(" \"chainId\": ").append(CHAIN).append(",\n \"bindHeight\": ").append(BIND) + .append(",\n \"identityBlock\": ").append(H_ID).append(",\n \"keysFrom\": ").append(H_ID) + .append(",\n \"finalityBlock\": ").append(H_FIN).append(",\n \"rows\": [\n"); + for (int i = 0; i < N; i++) { + j.append(" {\"index\": ").append(i).append(", \"id\": \"").append(hex(ids.get(i).getBytes())) + .append("\", \"falcon\": \"").append(hex(Bytes.wrap(std.get(i)))) + .append("\", \"ecdsa\": \"").append(hex(Util.publicKeyToAddress(ecdsa.get(i).getPublicKey()).getBytes())) + .append("\"}").append(i + 1 < N ? ",\n" : "\n"); + } + j.append(" ],\n \"messages\": [\n"); + for (int k = 0; k < out.size(); k++) { + final String[] m = out.get(k); + j.append(" {\"name\": \"").append(m[0]).append("\", \"type\": \"").append(m[1]).append("\", \"height\": ").append(m[2]) + .append(", \"wire\": \"").append(m[3]).append("\", \"author\": \"").append(m[4]).append("\"}") + .append(k + 1 < out.size() ? ",\n" : "\n"); + } + j.append(" ]\n}\n"); + final Path dir = Path.of(System.getenv("AERE_T4_VECTORS_OUT")); + Files.createDirectories(dir); + Files.writeString(dir.resolve("author-t4.json"), j.toString(), StandardCharsets.UTF_8); + } + + private static Optional seal( + final List falcon, final int index, final int signer, final Bytes32 message) { + return Optional.of( + new FalconSeal(index, Bytes.wrap(FALCON.sign(falcon.get(signer), message.toArrayUnsafe()).orElseThrow()))); + } +} diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuardTest.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuardTest.java new file mode 100644 index 0000000..fffe7b6 --- /dev/null +++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuardTest.java @@ -0,0 +1,130 @@ +/* + * 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.core.validation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.hyperledger.besu.consensus.common.bft.AerePqIdentity; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * AIP-22 stage 4: H_fin is refused over a seal layer that is not verified from H_fin (AERE-PQC-FIN-02), + * named layer by layer; H_fin unset is inert. The last probe goes through the production path (the + * four properties the layers really read). The runner's negative control plants "never refuse" at run + * time and these probes turn red. + */ +public class PqFinalityLayersGuardTest { + + private static final long H_FIN = 5_000L; + private static final String[] PROPS = { + PqProposalEnforcement.PROPERTY_FORK_BLOCK, + PqPrepareEnforcement.PROPERTY_FORK_BLOCK, + PqCommitEnforcement.PROPERTY_FORK_BLOCK, + PqRoundChangeEnforcement.PROPERTY_FORK_BLOCK + }; + + @AfterEach + public void clear() { + for (final String p : PROPS) { + System.clearProperty(p); + } + } + + private static PqFinalityLayersGuard.Layer armedFrom(final String property, final long from) { + return new PqFinalityLayersGuard.Layer(property, h -> h >= from); + } + + private static List allArmedFrom(final long from) { + return List.of( + armedFrom(PROPS[0], from), armedFrom(PROPS[1], from), armedFrom(PROPS[2], from), + armedFrom(PROPS[3], from)); + } + + @Test + public void everyLayerArmedAtOrBelowHfinPassesAndHfinUnsetIsInert() { + assertThat(PqFinalityLayersGuard.refusal(H_FIN, allArmedFrom(H_FIN))).isEmpty(); + assertThat(PqFinalityLayersGuard.refusal(H_FIN, allArmedFrom(100))).isEmpty(); + final List none = + List.of( + new PqFinalityLayersGuard.Layer(PROPS[0], null), + new PqFinalityLayersGuard.Layer(PROPS[1], null), + new PqFinalityLayersGuard.Layer(PROPS[2], null), + new PqFinalityLayersGuard.Layer(PROPS[3], null)); + assertThat(PqFinalityLayersGuard.refusal(AerePqIdentity.NEVER, none)).isEmpty(); + } + + @Test + public void aLayerUnsetOrArmedAboveHfinIsRefusedByName() { + for (int i = 0; i < PROPS.length; i++) { + final List unset = new java.util.ArrayList<>(allArmedFrom(100)); + unset.set(i, new PqFinalityLayersGuard.Layer(PROPS[i], null)); + final Optional r = PqFinalityLayersGuard.refusal(H_FIN, unset); + assertThat(r).as("layer %s unset", PROPS[i]).isPresent(); + assertThat(r.get()).startsWith("AERE-PQC-FIN-02").contains(PROPS[i] + " unset"); + + final List late = new java.util.ArrayList<>(allArmedFrom(100)); + late.set(i, armedFrom(PROPS[i], H_FIN + 1)); + final Optional r2 = PqFinalityLayersGuard.refusal(H_FIN, late); + assertThat(r2).as("layer %s armed one block above H_fin", PROPS[i]).isPresent(); + assertThat(r2.get()).contains(PROPS[i] + " armed above " + H_FIN); + } + } + + @Test + public void productionPathReadsTheFourPropertiesTheLayersRead() { + for (final String p : PROPS) { + System.setProperty(p, Long.toString(H_FIN - 10)); + } + PqFinalityLayersGuard.verifyOrAbort(H_FIN); + System.clearProperty(PqRoundChangeEnforcement.PROPERTY_FORK_BLOCK); + assertThatThrownBy(() -> PqFinalityLayersGuard.verifyOrAbort(H_FIN)) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("AERE-PQC-FIN-02") + .hasMessageContaining(PqRoundChangeEnforcement.PROPERTY_FORK_BLOCK + " unset"); + // H_fin unset: inert whatever the layers + PqFinalityLayersGuard.verifyOrAbort(AerePqIdentity.NEVER); + } + + @Test + public void aSealingNodeMustSealAllFourMessagesAtOrBelowHfin() { + final long never = Long.MAX_VALUE; + assertThat(PqFinalityLayersGuard.emissionRefusal(AerePqIdentity.NEVER, true, never, never, never, never)) + .as("unset H_fin asks nothing") + .isEmpty(); + assertThat(PqFinalityLayersGuard.emissionRefusal(H_FIN, false, never, never, never, never)) + .as("a reader seals nothing and needs nothing") + .isEmpty(); + assertThat(PqFinalityLayersGuard.emissionRefusal(H_FIN, true, 32, 200, 200, 200)).isEmpty(); + assertThat(PqFinalityLayersGuard.emissionRefusal(H_FIN, true, H_FIN, H_FIN, H_FIN, H_FIN)).isEmpty(); + assertThat(PqFinalityLayersGuard.emissionRefusal(H_FIN, true, 32, never, 200, 200)) + .hasValueSatisfying( + r -> assertThat(r).startsWith("AERE-PQC-FIN-03").contains("aere.pq.proposalPq.attachBlock unset")); + assertThat(PqFinalityLayersGuard.emissionRefusal(H_FIN, true, 32, 200, H_FIN + 1, 200)) + .hasValueSatisfying(r -> assertThat(r).contains("aere.pq.preparePq.attachBlock armed above H_fin")); + assertThat(PqFinalityLayersGuard.emissionRefusal(H_FIN, true, 32, 200, 200, never)) + .hasValueSatisfying(r -> assertThat(r).contains("aere.pq.roundChangePq.attachBlock unset")); + assertThat(PqFinalityLayersGuard.emissionRefusal(H_FIN, true, H_FIN + 5, 200, 200, 200)) + .hasValueSatisfying(r -> assertThat(r).contains("aere.falcon.attachBlock armed above H_fin")); + assertThatThrownBy(() -> PqFinalityLayersGuard.verifyEmissionOrAbort(H_FIN, true, 32, never, 200, 200)) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("AERE-PQC-FIN-03"); + } +} 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 cfc67a9..d2bc042 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 @@ -33,6 +33,7 @@ import org.hyperledger.besu.consensus.qbft.headervalidationrules.FalconSealValid 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.PqEmergencyShoutRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.PqRegistryBindingRule; import org.hyperledger.besu.consensus.qbft.headervalidationrules.QbftValidatorsValidationRule; @@ -188,7 +189,11 @@ public class QbftBlockHeaderValidationRulesetFactory { // that once, against the head that existed at startup; a rotation entry in // the schedule can pass underneath a running node and never be noticed. // Inert when no schedule is configured, which is chain 2800 today. - .addRule(new PqRegistryBindingRule()); + .addRule(new PqRegistryBindingRule()) + // AERE AIP-22 stage 4: the checkpoint (aere.pq.checkpoint=:); a header at a + // checkpoint height with another hash is refused. Attached, so it runs at import. Inert + // when unset, which is every chain today; a malformed value refuses at load. + .addRule(PqCheckpointRule.fromSystemConfiguration()); // Currently the minimum acceptable time between blocks is 1 second. The timestamp of an // Ethereum header is stored as seconds since Unix epoch so blocks being produced more diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRule.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRule.java new file mode 100644 index 0000000..d6b46c5 --- /dev/null +++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRule.java @@ -0,0 +1,133 @@ +/* + * 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.datatypes.Hash; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.mainnet.AttachedBlockHeaderValidationRule; + +import java.util.Collections; +import java.util.NavigableMap; +import java.util.TreeMap; + +import com.google.common.base.Splitter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AERE AIP-22 stage 4 (plan 9.6.7 point iv): the CHECKPOINT, which is how the weak subjectivity of a + * finality gadget is handled, stated publicly rather than hidden. A node that synchronises from + * genesis without other knowledge can be handed, by an eclipsing neighbour, a fork that ends before + * the first certified anchor; it can never reach the honest chain's height with valid certificates, + * but the eclipsed node does not know that height. Given a recent anchor from outside (height and + * block hash, from the published registry epoch or from its operator), it refuses any header at that + * height with another hash, so every chain it accepts runs through the checkpoint, and a rewrite at + * any height below it would have to reproduce every certificate above. + * + *

{@code aere.pq.checkpoint=:[,:...]}. Unset or blank: + * inert (every header passes), which is every chain as it stands. A value it cannot parse refuses at + * load (AERE-PQC-CHECKPOINT-01), never silently degrades to "no checkpoint". ATTACHED, so it runs at + * import under SKIP_DETACHED, the mode full sync imports with (the same reason R1A exists). + */ +public class PqCheckpointRule implements AttachedBlockHeaderValidationRule { + + /** The system property that names the checkpoints. */ + public static final String PROPERTY = "aere.pq.checkpoint"; + + private static final Logger LOG = LoggerFactory.getLogger(PqCheckpointRule.class); + + private final NavigableMap checkpoints; + + /** + * A rule over the given checkpoints (height to block hash). + * + * @param checkpoints the checkpoints; empty means inert + */ + public PqCheckpointRule(final NavigableMap checkpoints) { + this.checkpoints = Collections.unmodifiableNavigableMap(new TreeMap<>(checkpoints)); + } + + /** + * The rule configured on this node by {@link #PROPERTY}. + * + * @return the rule + */ + public static PqCheckpointRule fromSystemConfiguration() { + return new PqCheckpointRule(parse(System.getProperty(PROPERTY))); + } + + /** + * The checkpoints named by this node's configuration. + * + * @return height to block hash, empty when unset + */ + public NavigableMap checkpoints() { + return checkpoints; + } + + /** + * Parses {@code :[,...]}; fail-closed on anything malformed. + * + * @param value the property value, possibly null + * @return height to block hash + */ + static NavigableMap parse(final String value) { + final TreeMap m = new TreeMap<>(); + if (value == null || value.isBlank()) { + return m; + } + for (final String part : Splitter.on(',').trimResults().omitEmptyStrings().split(value)) { + final int c = part.indexOf(':'); + if (c <= 0) { + throw new IllegalArgumentException( + "AERE-PQC-CHECKPOINT-01: " + PROPERTY + " entry '" + part + "' is not :"); + } + final long height; + final Hash hash; + try { + height = Long.parseLong(part.substring(0, c).trim()); + hash = Hash.fromHexString(part.substring(c + 1).trim()); + } catch (final RuntimeException e) { + throw new IllegalArgumentException( + "AERE-PQC-CHECKPOINT-01: " + PROPERTY + " entry '" + part + "' is not :<32-byte block hash>", e); + } + if (height < 1) { + throw new IllegalArgumentException( + "AERE-PQC-CHECKPOINT-01: " + PROPERTY + " height " + height + " must be at least 1"); + } + if (m.put(height, hash) != null) { + throw new IllegalArgumentException( + "AERE-PQC-CHECKPOINT-01: " + PROPERTY + " names height " + height + " twice"); + } + } + return m; + } + + @Override + public boolean validate( + final BlockHeader header, final BlockHeader parent, final ProtocolContext protocolContext) { + final Hash want = checkpoints.get(header.getNumber()); + if (want == null || want.equals(header.getHash())) { + return true; + } + LOG.warn( + "AERE-PQC-CHECKPOINT-02: block {} has hash {} but this node's checkpoint at that height is {}; REFUSED (a chain that does not run through the checkpoint is never adopted)", + header.getNumber(), + header.getHash(), + want); + return false; + } +} diff --git a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRuleTest.java b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRuleTest.java new file mode 100644 index 0000000..dfdc786 --- /dev/null +++ b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRuleTest.java @@ -0,0 +1,75 @@ +/* + * 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.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; + +import java.util.TreeMap; + +import org.junit.jupiter.api.Test; + +/** + * AIP-22 stage 4: the checkpoint rule. At the checkpoint height only the checkpoint's hash passes; + * every other height passes; unset is inert; a malformed value refuses at load with its reason. The + * negative control of the runner plants "never refuse" at run time and turns the refusal probe red. + */ +class PqCheckpointRuleTest { + + private static BlockHeader header(final long number, final long timestamp) { + return new BlockHeaderTestFixture().number(number).timestamp(timestamp).buildHeader(); + } + + @Test + void atTheCheckpointHeightOnlyTheCheckpointHashPassesAndAnotherHashIsRefused() { + final BlockHeader honest = header(2048, 1000); + final BlockHeader fork = header(2048, 1001); + assertThat(fork.getHash()).isNotEqualTo(honest.getHash()); + final TreeMap cp = new TreeMap<>(); + cp.put(2048L, honest.getHash()); + final PqCheckpointRule rule = new PqCheckpointRule(cp); + assertThat(rule.validate(honest, null, null)).isTrue(); + assertThat(rule.validate(fork, null, null)).isFalse(); + // other heights are not judged by the checkpoint + assertThat(rule.validate(header(2047, 999), null, null)).isTrue(); + assertThat(rule.validate(header(2049, 1002), null, null)).isTrue(); + } + + @Test + void unsetIsInertAndTheParserReadsSeveralCheckpoints() { + final PqCheckpointRule inert = new PqCheckpointRule(PqCheckpointRule.parse(null)); + assertThat(inert.checkpoints()).isEmpty(); + assertThat(inert.validate(header(2048, 1001), null, null)).isTrue(); + assertThat(PqCheckpointRule.parse(" ")).isEmpty(); + final String h1 = header(10, 1).getHash().toHexString(); + final String h2 = header(20, 2).getHash().toHexString(); + assertThat(PqCheckpointRule.parse("10:" + h1 + ", 20:" + h2)).hasSize(2).containsKeys(10L, 20L); + } + + @Test + void aMalformedValueRefusesAtLoadWithItsReason() { + final String h = header(10, 1).getHash().toHexString(); + for (final String bad : new String[] {"10", "x:" + h, "10:0x1234", "0:" + h, "10:" + h + ",10:" + h}) { + assertThatThrownBy(() -> PqCheckpointRule.parse(bad)) + .as(bad) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("AERE-PQC-CHECKPOINT-01"); + } + } +}