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:
parent
099908a4ca
commit
71412b9792
13
LICENSING.md
13
LICENSING.md
@ -132,8 +132,8 @@ repository, and the publisher refuses to ship a package whose list disagrees wit
|
||||
You do not need that tool to check the list: every row below names a file in this package, and
|
||||
`grep -i copyright` on it shows the same line.
|
||||
|
||||
Measured 2026-09-23 over `anchor/`: **189 files**. **90** carry someone
|
||||
else's copyright alone, **45** carry a shared notice naming both upstream and us, and **11** name only
|
||||
Measured 2026-09-23 over `anchor/`: **198 files**. **90** carry someone
|
||||
else's copyright alone, **54** carry a shared notice naming both upstream and us, and **11** name only
|
||||
`contributors to the AERE Network`; 43 carry no copyright line at all.
|
||||
The first two groups are the table below. A shared notice is listed too: those are precisely the
|
||||
upstream files we changed, so section 4(b) has something to say about each of them.
|
||||
@ -149,6 +149,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
|
||||
| `anchor/app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java` | `Copyright ConsenSys AG.` | upstream |
|
||||
| `anchor/consensus/common/build.gradle` | `Copyright ConsenSys AG.` | upstream |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java` | `Copyright ConsenSys AG.` | upstream |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java` | `Copyright ConsenSys AG.` | upstream |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java` | `Copyright ConsenSys AG.` | upstream |
|
||||
@ -165,6 +166,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealed.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java` | `Copyright ConsenSys AG.` | upstream |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
@ -191,6 +193,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream |
|
||||
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java` | `Copyright contributors to Besu.` | upstream |
|
||||
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java` | `Copyright contributors to Besu / Aere Network.` | upstream, **modified by Aere Network** |
|
||||
@ -228,6 +231,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
|
||||
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java` | `Copyright contributors to Besu.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java` | `Copyright 2020 ConsenSys AG.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/MessageValidator.java` | `Copyright ConsenSys AG.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuard.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcement.java` | `Copyright contributors to Besu.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqProposalEnforcement.java` | `Copyright contributors to Besu.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqRoundChangeEnforcement.java` | `Copyright contributors to Besu.` | upstream |
|
||||
@ -241,6 +245,9 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
|
||||
| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayloadPqTest.java` | `Copyright contributors to Besu.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqHybridExtrasNeverThrowTest.java` | `Copyright contributors to Hyperledger Besu.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqLateSealSalvageTest.java` | `Copyright contributors to Besu.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityMessageTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/AereFinalityVectorsWriter.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqFinalityLayersGuardTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqMessageAgilityTest.java` | `Copyright contributors to Besu.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareAgilityTest.java` | `Copyright contributors to Besu.` | upstream |
|
||||
| `anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcementTest.java` | `Copyright contributors to Besu.` | upstream |
|
||||
@ -259,6 +266,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
|
||||
| `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestAttachedRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealLogThrottleTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
@ -268,6 +276,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
|
||||
| `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqArmedWithoutRegistryTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqCheckpointRuleTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRuleTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqForkGateFeedTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
| `anchor/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java` | `Copyright ConsenSys AG.` | upstream |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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())));
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user