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

This commit is contained in:
Aere Network 2026-09-23 22:48:38 +03:00
parent 099908a4ca
commit 71412b9792
18 changed files with 1553 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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).
*
* <p>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.
*
* <p>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<FalconSeal> seal = sealed.getFalconSeal();
if (seal.isEmpty()) {
return NO_AUTHOR;
}
final int index = seal.get().getValidatorIndex();
final Optional<Address> id = AerePqIdentity.v3IdAt(height, index);
if (id.isEmpty()) {
return NO_AUTHOR;
}
try {
final Optional<Address> commitSigner = sealed.commitSealSigner();
if (commitSigner.isPresent()) {
return id.get().equals(AerePqIdentity.author(commitSigner.get(), height)) ? id.get() : NO_AUTHOR;
}
final Optional<Bytes32> 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;
}
}
}

View File

@ -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<FalconSeal> 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<Bytes32> 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<Address> commitSealSigner() {
return Optional.empty();
}
}

View File

@ -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<Address> id = new ArrayList<>();
private final List<Address> 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<FalconSeal> seal;
private final Optional<Address> commitSigner;
private final boolean messageThrows;
Sealed(final long height, final Optional<FalconSeal> seal, final Optional<Address> 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<FalconSeal> getFalconSeal() {
return seal;
}
@Override
public Optional<Bytes32> 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<Address> 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<FalconSeal> 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<FalconSeal> 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<Long, PqRegistryV3.Registry> 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<FalconSeal> 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");
}
}

View File

@ -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;
* <p>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<FalconSeal> 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<Address> 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.
*

View File

@ -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<FalconSeal> getFalconSeal() {
return falconSeal;
}
/** AIP-22 stage 4: the PREPARE seal's message, as PqPrepareEnforcement builds it. */
@Override
public Optional<Bytes32> pqSealMessage(final long chainId) {
return Optional.of(
PqAnchor.prepareMessage(
chainId,
roundIdentifier.getSequenceNumber(),
roundIdentifier.getRoundNumber(),
digest.getBytes()));
}
@Override
public ConsensusRoundIdentifier getRoundIdentifier() {
return roundIdentifier;

View File

@ -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<FalconSeal> 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<Bytes32> pqSealMessage(final long chainId) {
return Optional.of(
PqAnchor.proposalMessage(
chainId,
roundIdentifier.getSequenceNumber(),
roundIdentifier.getRoundNumber(),
proposedBlock.getHash().getBytes()));
}
@Override
public int getMessageType() {
return TYPE;

View File

@ -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> preparedRoundMetadata;
@ -115,10 +118,27 @@ public class RoundChangePayload extends QbftPayload {
*
* @return the seal, or empty
*/
@Override
public Optional<FalconSeal> getFalconSeal() {
return falconSeal;
}
/** AIP-22 stage 4: the ROUND-CHANGE seal's message, prepared or not, as PqRoundChangeEnforcement builds it. */
@Override
public Optional<Bytes32> 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)

View File

@ -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.
*
* <p>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<String> refusal(final long hFin, final List<Layer> layers) {
if (hFin == AerePqIdentity.NEVER) {
return Optional.empty();
}
final List<String> 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<Layer> 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<String> 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<String> 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<String> 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<String> 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<String> r =
emissionRefusal(hFin, signs, attachCommit, attachProposal, attachPrepare, attachRoundChange);
if (r.isPresent()) {
throw new IllegalStateException(r.get());
}
}
}

View File

@ -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<NodeKey> ecdsa = new ArrayList<>();
private final List<SealScheme.PrivateHandle> falcon = new ArrayList<>();
private final List<Address> ids = new ArrayList<>();
private @Mock QbftBlockCodec blockCodec;
@BeforeEach
public void registry() {
final List<PqRegistryV3.Row> 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<Integer, PqRegistryV3.Key> 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<Bytes32> 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<Long, PqRegistryV3.Registry> 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<FalconSeal> 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 <P extends Payload> 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<PreparedRoundMetadata> 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<FalconSeal> 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);
}
}

View File

@ -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<NodeKey> ecdsa = new ArrayList<>();
final List<SealScheme.PrivateHandle> falcon = new ArrayList<>();
final List<byte[]> std = new ArrayList<>();
final List<Address> ids = new ArrayList<>();
final List<PqRegistryV3.Row> 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<Integer, PqRegistryV3.Key> 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<Bytes32> 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<Long, PqRegistryV3.Registry> 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<String[]> 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<FalconSeal> seal(
final List<SealScheme.PrivateHandle> 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())));
}
}

View File

@ -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<PqFinalityLayersGuard.Layer> 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<PqFinalityLayersGuard.Layer> 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<PqFinalityLayersGuard.Layer> unset = new java.util.ArrayList<>(allArmedFrom(100));
unset.set(i, new PqFinalityLayersGuard.Layer(PROPS[i], null));
final Optional<String> 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<PqFinalityLayersGuard.Layer> late = new java.util.ArrayList<>(allArmedFrom(100));
late.set(i, armedFrom(PROPS[i], H_FIN + 1));
final Optional<String> 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");
}
}

View File

@ -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=<height>:<hash>); 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

View File

@ -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.
*
* <p>{@code aere.pq.checkpoint=<height>:<block hash>[,<height>:<block hash>...]}. 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<Long, Hash> checkpoints;
/**
* A rule over the given checkpoints (height to block hash).
*
* @param checkpoints the checkpoints; empty means inert
*/
public PqCheckpointRule(final NavigableMap<Long, Hash> 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<Long, Hash> checkpoints() {
return checkpoints;
}
/**
* Parses {@code <height>:<block hash>[,...]}; fail-closed on anything malformed.
*
* @param value the property value, possibly null
* @return height to block hash
*/
static NavigableMap<Long, Hash> parse(final String value) {
final TreeMap<Long, Hash> 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 <height>:<block hash>");
}
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 <height>:<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;
}
}

View File

@ -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<Long, Hash> 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");
}
}
}