anchor: the corpus carries the AIP-22 stage 1 to 3b code (per-scheme intervals, post-quantum validator identity, v3 key registry, key rotation under an id), unarmed on chain 2800 and proven on testnet 28001; the armed-on-2800 table says so row by row; LICENSING.md lists the new files
This commit is contained in:
parent
a94c0f2686
commit
099908a4ca
@ -132,9 +132,9 @@ repository, and the publisher refuses to ship a package whose list disagrees wit
|
|||||||
You do not need that tool to check the list: every row below names a file in this package, and
|
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.
|
`grep -i copyright` on it shows the same line.
|
||||||
|
|
||||||
Measured 2026-09-23 over `anchor/`: **179 files**. **90** carry someone
|
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
|
else's copyright alone, **45** carry a shared notice naming both upstream and us, and **11** name only
|
||||||
`contributors to the AERE Network`; 33 carry no copyright line at all.
|
`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
|
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.
|
upstream files we changed, so section 4(b) has something to say about each of them.
|
||||||
Every file below is Apache-2.0 material redistributed under section 4: it keeps its original
|
Every file below is Apache-2.0 material redistributed under section 4: it keeps its original
|
||||||
|
|||||||
@ -5,19 +5,20 @@ computed them, and the same 276 files once produced two different aggregates her
|
|||||||
|
|
||||||
329e4e2f93143cc8af092f0a951fa23619c12c4ec1793b053654f8e26e088da0 app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
|
329e4e2f93143cc8af092f0a951fa23619c12c4ec1793b053654f8e26e088da0 app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
|
||||||
3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java
|
3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java
|
||||||
e17f5b548f5a0833aef11148f561f2854dacf501bbe7c98f926349707ef77429 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
|
6a324fd14b4dea445ac08a7ef43bedb800c64c11c4bcbaefb58f98ec9051e60a app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
|
||||||
129493150c0b13e8020bdf50c0c1ae07268d4f73a69a821b303bf4894451fa5e app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java
|
129493150c0b13e8020bdf50c0c1ae07268d4f73a69a821b303bf4894451fa5e app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java
|
||||||
728c29c299edf6011f2c2798cbe930a043112e0e06b924e7edb54284ca44a758 config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java
|
728c29c299edf6011f2c2798cbe930a043112e0e06b924e7edb54284ca44a758 config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java
|
||||||
a3b93f2602e9755d91358c9b3f473235f4d40ae23d912f797cdb03ed7e1348f9 consensus/common/build.gradle
|
a3b93f2602e9755d91358c9b3f473235f4d40ae23d912f797cdb03ed7e1348f9 consensus/common/build.gradle
|
||||||
68d635954ef33448a661256f50642eeeff2f7bb5874a54ffc2e64193c83fe391 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelay.java
|
68d635954ef33448a661256f50642eeeff2f7bb5874a54ffc2e64193c83fe391 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelay.java
|
||||||
8fa317b1ba1d887f0623610f8721d728d954cc800d3b391729ed06f64c8141a6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.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
|
2e84bb48611d3da7c46bcd4018ae746cbf785dfcb16f495674a0f6d542e71e8e consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java
|
||||||
70fa067469a084ac225765da6a01619dec51130acc255b7194a1ad274d327141 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java
|
70fa067469a084ac225765da6a01619dec51130acc255b7194a1ad274d327141 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java
|
||||||
3ab425b5b5f7d7c2199065a3905623d5c1c4c5922d892c6c4275415b0c69a2b4 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
|
3ab425b5b5f7d7c2199065a3905623d5c1c4c5922d892c6c4275415b0c69a2b4 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
|
||||||
db1e80115ce59c8281fbf046cb37e153a49fe017c1675afd200e8125d3f6b538 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealScheme.java
|
db1e80115ce59c8281fbf046cb37e153a49fe017c1675afd200e8125d3f6b538 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealScheme.java
|
||||||
789db234a90f118cdf881653ee2bb7ad4a229a685315b04822147ffffe6a3c13 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java
|
dc4c68ad18d64aedae2363fc83f7a5329d258e749b9f7cbe145ba8496139785e consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java
|
||||||
e8c8111a343cc993b950ac4e5b3992f16b8ee3d34c8827f00dd80d017884335b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducer.java
|
570f7825afdb0dd10e1d920f7de8ae21a85f196e77edbd8da8de94994d845839 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducer.java
|
||||||
be99cd76f4b80a0c83e0a18801249c7972a9e93c54bbad6158d1155753a7ad6a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupport.java
|
dbb4381155c4222491676da2e6cd13a8b15ce38c59a14ee8cb65edd3af1bf282 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupport.java
|
||||||
681d5ca2ea929d81f92664341d2651160d50621efb5194808627fb61834c292b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistry.java
|
681d5ca2ea929d81f92664341d2651160d50621efb5194808627fb61834c292b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistry.java
|
||||||
ecd0e865aa48e461618048c13c46d9b54095dfa2e14180750f49b90faf49302a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
|
ecd0e865aa48e461618048c13c46d9b54095dfa2e14180750f49b90faf49302a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
|
||||||
1c3d90324c6f5116194bae0539680fdc1f528ba4c893c5e2b44a592aea6b3abd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java
|
1c3d90324c6f5116194bae0539680fdc1f528ba4c893c5e2b44a592aea6b3abd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java
|
||||||
@ -29,7 +30,8 @@ e16b2c654fe21893c7972dac71f3ffafd1e4e8efbecdb6f95051820e36fbd48e consensus/comm
|
|||||||
78ecea21ff746564984dff40e1067808789f9bc5075ca3b5e8af92e0ea137c72 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBinding.java
|
78ecea21ff746564984dff40e1067808789f9bc5075ca3b5e8af92e0ea137c72 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBinding.java
|
||||||
f67a205036550f7d0f1bfed2abe7b89abd73e80614adff8d696b33c580269a6f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHash.java
|
f67a205036550f7d0f1bfed2abe7b89abd73e80614adff8d696b33c580269a6f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHash.java
|
||||||
3fbd850e00e487c40c61310d894d6e1f765e9098e5c55438f9b06ab8fb92d45b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java
|
3fbd850e00e487c40c61310d894d6e1f765e9098e5c55438f9b06ab8fb92d45b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java
|
||||||
e85f4be696700184f9700cc531b4f3df313e9fb8cff8e2bc39d24241a769addb consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSchemeSchedule.java
|
11f1c2a97a40df999471977cb3eab9af4e816081b100e345e8c962ef3bbf76f5 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryV3.java
|
||||||
|
2ee139d21f73ff3417f6ad61b233e5ca3a95f92e68a4284eb99a47b1f8f4b58b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSchemeSchedule.java
|
||||||
c0c93a096f4eceab6bf92cc3618ce7cee1e54d2d20b6ac781850a390ec6e514f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java
|
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
|
73abbaddcec1533a7a758f1c4a26ae68a83ddbb96fd98ad147e74d7386bbbf3f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java
|
||||||
a235b3ec8c69665c525d7c74a084d589904879f5cd33cb87e1d1f38eabba8a15 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java
|
a235b3ec8c69665c525d7c74a084d589904879f5cd33cb87e1d1f38eabba8a15 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java
|
||||||
@ -37,8 +39,8 @@ addba40c0d931a3ecfa3b2f0179311dacaa604884e3ebb1d79f958bd913f72e2 consensus/comm
|
|||||||
285f6a7c1188a387fb1ac21f5ab809595ef48d936b40aced94047525e6def0a6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java
|
285f6a7c1188a387fb1ac21f5ab809595ef48d936b40aced94047525e6def0a6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java
|
||||||
3b57840dc103063b3797697feb78b90af93ca123587e9278cfb43b84edbd995a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealSchemes.java
|
3b57840dc103063b3797697feb78b90af93ca123587e9278cfb43b84edbd995a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealSchemes.java
|
||||||
444961503d515cb9f045729577e190941e59baa4bd90438a2da05f41fed7750a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SlhDsaSealScheme.java
|
444961503d515cb9f045729577e190941e59baa4bd90438a2da05f41fed7750a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SlhDsaSealScheme.java
|
||||||
b6c9ecbf3cd2ee73111984cd04a89c32ee56a1cc699a197b65ef7c10001b8cbc consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java
|
454eeca487505e70c7f0000f5401b6bf4530da1d5ce6bcb803427f1d08e3cb1c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java
|
||||||
15ee94346cd803530cf9362b74c645aad0986c6d26a5386f9f38ee4e92a66b7b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java
|
e89deb6f346c221affa717eafa464d497401b7b5a01c84c144dace42032879a9 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java
|
||||||
baa7f138046c501c5baecae3615d54ed0bbd6fea47d7f84a3d71eaa5698d182d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
|
baa7f138046c501c5baecae3615d54ed0bbd6fea47d7f84a3d71eaa5698d182d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
|
||||||
7c464b927fcb39ab017c543ca42bcd076b1dc69573c7ddefda1e685ebf9eb9ba consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
|
7c464b927fcb39ab017c543ca42bcd076b1dc69573c7ddefda1e685ebf9eb9ba consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
|
||||||
ddb17d7c6e1b762a9e8bcc924a17d42218cb5ec16528d80170bf2ff07e40aba2 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
|
ddb17d7c6e1b762a9e8bcc924a17d42218cb5ec16528d80170bf2ff07e40aba2 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
|
||||||
@ -61,10 +63,14 @@ c72f197a0f8967aec5e08d1c4bb30030b9dc16c27711567ca40ad0cbe845c989 consensus/comm
|
|||||||
636399666ed9490b29a4cb85d2b3cddf2ef4e8f8fa9a8ecf2315a8abf3e2e6de consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.java
|
636399666ed9490b29a4cb85d2b3cddf2ef4e8f8fa9a8ecf2315a8abf3e2e6de consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.java
|
||||||
bd6833075646ffa2f4fca2761e3d68ccbc37f7c5bb209ae2e40f8cfd469dce14 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java
|
bd6833075646ffa2f4fca2761e3d68ccbc37f7c5bb209ae2e40f8cfd469dce14 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java
|
||||||
6ace00e18914a1558563e689b7427654b5e6226a31447bef1d0416d993f78840 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java
|
6ace00e18914a1558563e689b7427654b5e6226a31447bef1d0416d993f78840 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java
|
||||||
|
ed1cc6f617dc5a2f7801b6e8f6f039a035a2ffc9542b116262dac39bb70563dd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryV3Tool.java
|
||||||
cf0dcc008ce2d040590329088abdcbfc0829bf529445422deee8022d4856e08a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelayTest.java
|
cf0dcc008ce2d040590329088abdcbfc0829bf529445422deee8022d4856e08a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelayTest.java
|
||||||
|
f5d6d26b9cd3babad8f7d16b9595497908b1f854d166837fbecd485d721ae856 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereKeysFromV3Test.java
|
||||||
|
1d7e3a30d3f4653b778a1bcbb81203ed8855d904017fa438a06e26baad8958c3 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereLocalKeysV3Test.java
|
||||||
|
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
|
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
|
c574ec22e642ac464b1728d27e4bb26202448a45c044f08935cc0a0efa0297d9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FalconAttachIntervalTest.java
|
||||||
0eb20506f851c1510bcaf659db8c5b0d384cdc60ed06192a03a665f0ee2b3912 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducerTest.java
|
e96c6e79287a17eafef8f4ff5c32e539e489115b02f7afaceb8cdeff73562d69 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducerTest.java
|
||||||
186c4c92199400783ee3424231818dd440b0da41aeb3d8bd643cd13759998250 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupportTest.java
|
186c4c92199400783ee3424231818dd440b0da41aeb3d8bd643cd13759998250 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupportTest.java
|
||||||
834861639fd119c1653e6a3977ffbe4c2d4dce63a5741f1fe5a087643b86ed74 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java
|
834861639fd119c1653e6a3977ffbe4c2d4dce63a5741f1fe5a087643b86ed74 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java
|
||||||
2920f18dae0eb7ccead43d80202580b116e28c9fae5bb75d54917ddb99b9c2e9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java
|
2920f18dae0eb7ccead43d80202580b116e28c9fae5bb75d54917ddb99b9c2e9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java
|
||||||
@ -90,7 +96,8 @@ f260d284f936307ac4c31142cbea0359ded1197880b8a960ac27dd7beb5c3b53 consensus/comm
|
|||||||
19ff0e11861d44b582fe39d09aaeb63ee0a5b9ba07d036da85a70ea612dab6dc consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java
|
19ff0e11861d44b582fe39d09aaeb63ee0a5b9ba07d036da85a70ea612dab6dc consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java
|
||||||
97281811ae1fe6fae8001d0d64e11d4779be153547ab24c779a54aba3e211c88 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHeightRefusalTest.java
|
97281811ae1fe6fae8001d0d64e11d4779be153547ab24c779a54aba3e211c88 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHeightRefusalTest.java
|
||||||
7ec10b4fa5ac09e4d980f5d180c75eecc892d95c2f81d97c6140e64ada34654d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java
|
7ec10b4fa5ac09e4d980f5d180c75eecc892d95c2f81d97c6140e64ada34654d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java
|
||||||
36cd369e8386cac85501f7d8a56f9604ec39e8bd89a8d754cabe85e4fb6b8e9a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java
|
05423fc69ce0e4e94cda0d56a181a996320bd2166af3415f2d88102cf3b0ae21 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryV3Test.java
|
||||||
|
763dcb09c30c2031ac19bb62ec7a8a16a2f8042fd0e3d04840867b08df5b1ba3 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java
|
||||||
e73a075b2acfce7a6d2de63130ed55a3d8f422bdc72c51325a010852064d406d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java
|
e73a075b2acfce7a6d2de63130ed55a3d8f422bdc72c51325a010852064d406d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java
|
||||||
e0bc7a465dbb2dd7efa5f1830211a1a45c6554c300b2088dd70bb9677b0ac217 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java
|
e0bc7a465dbb2dd7efa5f1830211a1a45c6554c300b2088dd70bb9677b0ac217 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java
|
||||||
9a371ec367cda846640c6da18c8bf7d921a6882cfc024dc0c86d4f965cae0c63 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java
|
9a371ec367cda846640c6da18c8bf7d921a6882cfc024dc0c86d4f965cae0c63 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java
|
||||||
@ -101,6 +108,7 @@ e355b147fdda48c14cb6dc09b1bc1e7a7ccae3f9b709af8f8483f896086dbe4d consensus/comm
|
|||||||
d48d62c6ea0784864a5987a804a28855265b2180c409ba3c5b4434d21b805e1d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java
|
d48d62c6ea0784864a5987a804a28855265b2180c409ba3c5b4434d21b805e1d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java
|
||||||
184e3cc6f2f9beb1bd972b077ab2ced456264a55296f810ba5ed2dc976195392 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SlhDsaCrossVectorTest.java
|
184e3cc6f2f9beb1bd972b077ab2ced456264a55296f810ba5ed2dc976195392 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SlhDsaCrossVectorTest.java
|
||||||
ae37d3525697bf84defd796bb8813193c78f81e7fa24a72f8f048285d52399e2 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SlhDsaFastEngineTest.java
|
ae37d3525697bf84defd796bb8813193c78f81e7fa24a72f8f048285d52399e2 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SlhDsaFastEngineTest.java
|
||||||
|
bcdd5c3c267f38eee9700bc6ba07d41b487c7889224820868e1839044108451f consensus/common/src/test/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTallyIdentityTest.java
|
||||||
6b6ecfcfb9b25bb009cb5d4df74648c729abf62626e5034af126771825b7418e consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Commit.java
|
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
|
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
|
03173f2029f767e10cc978b5576b9557efa783dab2ef105aa856f6d5e08b9352 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java
|
||||||
@ -113,9 +121,9 @@ efb0e336e19a16ef192e3d1f91f1e5488f632e17b5748654e75008ddfcdda932 consensus/qbft
|
|||||||
9c7e2698016f8cf19d3b6d78c6256ad5e8eabec06d9a877eee4e8c2dde423839 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
|
9c7e2698016f8cf19d3b6d78c6256ad5e8eabec06d9a877eee4e8c2dde423839 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
|
||||||
00c8204e4b44acd469973d6bbc07992382fdd2d692f079a55de76f944a7432f8 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java
|
00c8204e4b44acd469973d6bbc07992382fdd2d692f079a55de76f944a7432f8 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java
|
||||||
601b0cccf32f8ebe327c7581e0e9b3f9f67de49d9fb646326d6b49ef2138c641 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java
|
601b0cccf32f8ebe327c7581e0e9b3f9f67de49d9fb646326d6b49ef2138c641 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java
|
||||||
01ed4a44aa58da124efb6f4fcf479f9511c8f522f40ccf0be5fff3d0bcb38f34 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java
|
4c0811d182f5252bdde31560f514a518db58c8d0c82c748a6c0f5d68472470c6 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java
|
||||||
a40621537e2b2024ab5586c9001f96fd81ffe3c3d5ce1e867a07a77584cf2173 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/MessageValidator.java
|
a40621537e2b2024ab5586c9001f96fd81ffe3c3d5ce1e867a07a77584cf2173 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/MessageValidator.java
|
||||||
bebf53cbc7f3a9ca2807bd980d313f8131e96f79259ba41e80a598d0327b1a0d consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java
|
22c73f9cdfe5626347f34f173b7b60312e00970ccba49862686051b2dd84fe36 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java
|
||||||
5ab52433f770537e838b15e23abfc7b24eaa733ea26577451d5e32226d617879 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcement.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
|
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
|
afb487f8c1dd87e323fdc1153ecfdf8ec6cb5a10e0726783d4214c07a3fa9a95 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqRoundChangeEnforcement.java
|
||||||
@ -131,12 +139,13 @@ df5d7d2119cf0864a8c2dce50f4f69bd8b64804cfda7f7366c013b304af0a6e7 consensus/qbft
|
|||||||
97ac1e46ab37238a2b6232eccdd4636ac6fa928c8e3d4f8a473c083c5d5c3b39 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqHybridExtrasNeverThrowTest.java
|
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
|
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
|
127de5fbe8014768a657f31bc77d7e66557273febdf3412a5536e8628fd70c5c consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqSealsWithoutProposalTest.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
|
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
|
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
|
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
|
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
|
c0853ed97c53d54951e25ad6d0b70c0299dedd859cc7c44da64e3751d0e0de33 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitPlumbingTest.java
|
||||||
61f6f0ca66cabbad7e57d7eccf86a6d4f5434f0769726db24efec38c9503583d consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqHybridEnforcementTest.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
|
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
|
5a47d247d7bcda77b57f5c906c3cee1af826785416012fafe9cfa49d63671388 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareAgilityTest.java
|
||||||
1939e33cc8ea81782e5e17d68e2b59c001e4207ed8ef07acd1d2a166a047dcb3 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcementTest.java
|
1939e33cc8ea81782e5e17d68e2b59c001e4207ed8ef07acd1d2a166a047dcb3 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcementTest.java
|
||||||
@ -155,9 +164,10 @@ cbec6f4e280250ddefe5c79c453b59a07be6435e6f88d5614e92168c855e4693 consensus/qbft
|
|||||||
e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRule.java
|
e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRule.java
|
||||||
ab0fcd8722dcb76560f0ef8fda8af9c2b8b6ec9b468326aaf616c853526f7f81 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestAttachedRule.java
|
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
|
63313f944fcda0db45c40d4df840a465e6147cc85997b8ca832f0cfefea97987 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java
|
||||||
bfea357d0f539ae7d2d6fabf2ea343e0bce3d3faf2cea57c6d5e97c75552371b consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
|
0f348ac4fa519f27f52f631dd290eab6e84980739a09ec767c8ca69e4a4f8f64 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
|
||||||
2add2a7733668e11b50ddd64b05bb37db26a18284a636bbe720c2cb1f6aa28ed consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRule.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
|
8ac99e94c89f61f0281199cf369e282fc4f9fb1a3414a1650766e1d7548e76f5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java
|
||||||
|
5dcf5c22389c063e12363b83366b39bd3b2e79f36c68e777fb3ef11080f53ea9 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/AereIdentityHeaderTest.java
|
||||||
52b56f157500ae3527b7e55c51786915bb5d6980065668f1e7b8e297bf7125ef consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftAnchorRuleWiringTest.java
|
52b56f157500ae3527b7e55c51786915bb5d6980065668f1e7b8e297bf7125ef consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftAnchorRuleWiringTest.java
|
||||||
5a18c7fee308654d9557fc507ca7a64704bdac5d13835e7df47a3dfb41519902 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRuleTest.java
|
5a18c7fee308654d9557fc507ca7a64704bdac5d13835e7df47a3dfb41519902 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRuleTest.java
|
||||||
ad018cba0a3fe7f018b11c6c6a2d45e3f5547342ec45b620a4df2595801ae71a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealLogThrottleTest.java
|
ad018cba0a3fe7f018b11c6c6a2d45e3f5547342ec45b620a4df2595801ae71a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealLogThrottleTest.java
|
||||||
@ -166,7 +176,7 @@ e2df575ee4d6ab5bd961b0886ece3d3c392a50193f1d3256a438a72da9e20d10 consensus/qbft
|
|||||||
77d16fd35ca5607e9702f39cfd9a24a7e8523f631a73e82138763e6d5b718e7b consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java
|
77d16fd35ca5607e9702f39cfd9a24a7e8523f631a73e82138763e6d5b718e7b consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java
|
||||||
a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java
|
a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java
|
||||||
535cca4f1083c8a25bd29877e988b99704666853c1fe8b0ba32d71efe4e15f37 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java
|
535cca4f1083c8a25bd29877e988b99704666853c1fe8b0ba32d71efe4e15f37 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java
|
||||||
612d5c7578a918441265a6ad99bf05e9c5e94eec743c375ffab03d1b19e2f797 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorV2RulesTest.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
|
321cb7e6923f77e078b523316650ac49136cf8d4df506b9bb235a32795c69611 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqArmedWithoutRegistryTest.java
|
||||||
484bc74ff3d1e52d25631264d83af70c2b695e973c7360b0f3db471ab5c6613a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRuleTest.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
|
49c3fd97e7985966531928af173b6f38ac48ff9dc8b5be1106e5b7bfb9c7b2b6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqForkGateFeedTest.java
|
||||||
|
|||||||
@ -176,6 +176,9 @@ This matters more than the file list, so it is stated before it.
|
|||||||
| Post-quantum seal on COMMIT, emission / enforcement | yes | **yes**, since the anchor / **armed for block 17,250,000** |
|
| Post-quantum seal on COMMIT, emission / enforcement | yes | **yes**, since the anchor / **armed for block 17,250,000** |
|
||||||
| Hybrid Falcon + SLH-DSA certificate (v2) | yes | **yes**, since block 17,047,600 (2026-09-04) |
|
| Hybrid Falcon + SLH-DSA certificate (v2) | yes | **yes**, since block 17,047,600 (2026-09-04) |
|
||||||
| Anchor interval 128 (`aere.pq.anchorIntervalSchedule`) | yes | **armed for block 17,225,968** (set 2026-09-05) |
|
| Anchor interval 128 (`aere.pq.anchorIntervalSchedule`) | yes | **armed for block 17,225,968** (set 2026-09-05) |
|
||||||
|
| 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 |
|
||||||
|
|
||||||
Everything not yet in force is **disarmed by absence or by height, never by a flag**: the properties that arm them
|
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
|
are unset, and unset means never. Each refuses loudly on a value it cannot parse rather than booting
|
||||||
@ -197,7 +200,8 @@ proposal without a valid Falcon seal from its own proposer does not open a round
|
|||||||
its own domain over (chainId, height, round, digest), so an honest proposal seal cannot be replayed
|
its own domain over (chainId, height, round, digest), so an honest proposal seal cannot be replayed
|
||||||
as a vote nor a vote seal as a proposal - both directions are tested. The ROUND-CHANGE message
|
as a vote nor a vote seal as a proposal - both directions are tested. The ROUND-CHANGE message
|
||||||
carries its own seal since 2026-08-31 (its embedded justifications are coupled to PREPARE
|
carries its own seal since 2026-08-31 (its embedded justifications are coupled to PREPARE
|
||||||
enforcement). What remains classical: node-level devp2p authentication and validator identity.
|
enforcement). What remains classical on chain 2800: node-level devp2p authentication and validator identity (the
|
||||||
|
post-quantum identity of AIP-22 is in this directory, unarmed on chain 2800, and runs only on testnet 28001).
|
||||||
|
|
||||||
### The files
|
### The files
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import org.hyperledger.besu.config.QbftFork;
|
|||||||
import org.hyperledger.besu.consensus.common.BftValidatorOverrides;
|
import org.hyperledger.besu.consensus.common.BftValidatorOverrides;
|
||||||
import org.hyperledger.besu.consensus.common.EpochManager;
|
import org.hyperledger.besu.consensus.common.EpochManager;
|
||||||
import org.hyperledger.besu.consensus.common.ForksSchedule;
|
import org.hyperledger.besu.consensus.common.ForksSchedule;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftBlockInterface;
|
import org.hyperledger.besu.consensus.common.bft.BftBlockInterface;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftEventQueue;
|
import org.hyperledger.besu.consensus.common.bft.BftEventQueue;
|
||||||
@ -433,6 +434,36 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder {
|
|||||||
// about what is actually configured on this node.
|
// about what is actually configured on this node.
|
||||||
final PqAnchorConfig aereAnchorConfig = PqAnchorConfig.fromSystemConfiguration();
|
final PqAnchorConfig aereAnchorConfig = PqAnchorConfig.fromSystemConfiguration();
|
||||||
|
|
||||||
|
// AERE AIP-22 stage 3: the post-quantum identity switch, read ONCE here beside the anchor
|
||||||
|
// configuration; the schemes every v3 row must carry are the ones the scheme schedule names at
|
||||||
|
// H_id. Inert when aere.pq.identityBlock is unset.
|
||||||
|
AerePqIdentity.installFromSystemProperties(
|
||||||
|
aereAnchorConfig.chainId(),
|
||||||
|
hId ->
|
||||||
|
HybridSealSupport.instance()
|
||||||
|
.schedule()
|
||||||
|
.map(s -> (java.util.List<String>) new java.util.ArrayList<>(s.schemesAt(hId)))
|
||||||
|
.orElse(java.util.List.of("falcon-512")));
|
||||||
|
// AERE AIP-22 stage 3b: at H_k the SOURCE of the keys that seal moves to the v3 epochs; the continuity
|
||||||
|
// guard proves, before the node joins anything, that no key moves with it (every key of the epoch at
|
||||||
|
// H_k equals the key the existing registries give at H_k - 1). Inert when H_k is not configured.
|
||||||
|
AerePqIdentity.verifyKeyContinuity(
|
||||||
|
(index, scheme) -> {
|
||||||
|
final long before =
|
||||||
|
AerePqIdentity.installed()
|
||||||
|
.map(c -> c.keysFrom() - 1L)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("identity not installed"));
|
||||||
|
return "falcon-512".equals(scheme)
|
||||||
|
? FalconSealSupport.instance().publicKeyBytesAtHistoric(before, index)
|
||||||
|
: HybridSealSupport.instance().registry().flatMap(r -> r.publicKey(index, scheme));
|
||||||
|
});
|
||||||
|
// AERE AIP-22 stage 3b-ii: in every v3 epoch from H_k where this node is a registered validator it must
|
||||||
|
// hold its row's keys, or it would go mute at that epoch; refuse to start instead. Inert without H_k.
|
||||||
|
FalconSealSupport.instance()
|
||||||
|
.verifyLocalKeysCoverV3EpochsOrAbort(Util.publicKeyToAddress(nodeKey.getPublicKey()));
|
||||||
|
HybridSealSupport.instance()
|
||||||
|
.verifyLocalKeysCoverV3EpochsOrAbort(Util.publicKeyToAddress(nodeKey.getPublicKey()));
|
||||||
|
|
||||||
PqAnchorSyncModeGuard.verifySyncModeOrAbort(
|
PqAnchorSyncModeGuard.verifySyncModeOrAbort(
|
||||||
syncConfig == null || syncConfig.getSyncMode() == null
|
syncConfig == null || syncConfig.getSyncMode() == null
|
||||||
? null
|
? null
|
||||||
@ -487,6 +518,30 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder {
|
|||||||
+ "node starts with an empty cache, exactly as before this repair.",
|
+ "node starts with an empty cache, exactly as before this repair.",
|
||||||
e.toString());
|
e.toString());
|
||||||
}
|
}
|
||||||
|
// AIP-21, SECOND HALF (2026-09-18): the finality WINDOW survives the restart too. Same
|
||||||
|
// security argument as above, one step further: a journal frame counts only if its hash is
|
||||||
|
// the canonical hash THIS chain holds at that height, and each seal only if it verifies now
|
||||||
|
// against the registry epoch bound there. A forged journal buys an empty window.
|
||||||
|
try {
|
||||||
|
final BlockHeader aereCapFereastra = blockchain.getChainHeadHeader();
|
||||||
|
final int aereFereastraRestaurata =
|
||||||
|
PqSealCache.instance()
|
||||||
|
.restoreWindowFromDisk(
|
||||||
|
aereCapFereastra.getNumber(),
|
||||||
|
aereInaltime ->
|
||||||
|
blockchain.getBlockHeader(aereInaltime).map(BlockHeader::getHash),
|
||||||
|
PqSignerRegistry.falconSealSupport());
|
||||||
|
LOG.info(
|
||||||
|
"AERE FEREASTRA-SIGILII (AIP-21): {} VERIFIED seal(s) restored into the finality window "
|
||||||
|
+ "below chain head {}; aere_getPqFinality answers for those blocks again.",
|
||||||
|
aereFereastraRestaurata,
|
||||||
|
aereCapFereastra.getNumber());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE FEREASTRA-SIGILII (AIP-21): the finality window could not be restored ({}); the "
|
||||||
|
+ "node answers 'unavailable' for blocks heard before this restart, as before.",
|
||||||
|
e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final BftExecutors bftExecutors =
|
final BftExecutors bftExecutors =
|
||||||
|
|||||||
@ -0,0 +1,561 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3: the post-quantum validator identity, in ONE place.
|
||||||
|
*
|
||||||
|
* WHAT IT DOES. From the activation height H_id, a validator is its 20-byte id (PqRegistryV3), not its
|
||||||
|
* ECDSA address. Upstream QBFT works entirely on two 20-byte things - the AUTHOR of a signed message and
|
||||||
|
* the validator LIST - so the switch is a TRANSLATION applied at the few places where an ECDSA address
|
||||||
|
* is born, and nowhere else (the choke points are listed in the AIP-22 plan, section 9.4):
|
||||||
|
*
|
||||||
|
* author(recovered, h) the author of a message or commit seal at height h: the id of the v3 row
|
||||||
|
* whose ecdsa.addr is the recovered address when h >= H_id, the address itself
|
||||||
|
* otherwise (an address with no row is then simply not in the set, and its
|
||||||
|
* message does not count - the upstream checks already say so)
|
||||||
|
* translateSet(set, b) the validator set after block b = H_id - 1 becomes the sorted ids of its
|
||||||
|
* members (VoteTallyUpdater), so extraData(H_id) carries ids and H_id is the
|
||||||
|
* first block sealed by ids; a member without a v3 row is a configuration
|
||||||
|
* FAULT that throws, never a silent drop
|
||||||
|
* local(address, h) this node's own identity at height h (coinbase, proposer check)
|
||||||
|
* isLocal(candidate, a) whether a 20-byte value is this node, in either form
|
||||||
|
* transportTargets(ids) the RLPx node-key addresses to send consensus messages to, from the rows'
|
||||||
|
* transport field; empty when some id lists none (send to every peer then)
|
||||||
|
*
|
||||||
|
* DISARMED unless installed: with no configuration every function is the identity, so a build that
|
||||||
|
* carries this class and is never configured behaves exactly as before, byte for byte.
|
||||||
|
*
|
||||||
|
* Configuration (system properties, like every AERE rule, read once at startup):
|
||||||
|
* aere.pq.identityBlock H_id
|
||||||
|
* aere.pq.registryV3 comma list of v3 manifests (JSON), each bound at its own bindHeight
|
||||||
|
* aere.pq.registryV3.hashes comma list "bindHeight:0xhash" - the pinned hash of every epoch; an
|
||||||
|
* epoch without a pin refuses startup (a registry nobody pinned is a file)
|
||||||
|
*
|
||||||
|
* NOT here (stated): the PQ seal registry lookups (index -> address) move to ids through the v3 epoch
|
||||||
|
* entering the registry schedule; the per-message PQ seal authorship check (SPEC 2.6 rule 2) then
|
||||||
|
* compares id with id.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NavigableMap;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/** The AIP-22 identity translation. */
|
||||||
|
public final class AerePqIdentity {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AerePqIdentity.class);
|
||||||
|
|
||||||
|
/** H_id. */
|
||||||
|
public static final String PROPERTY_BLOCK = "aere.pq.identityBlock";
|
||||||
|
|
||||||
|
/** The v3 manifests. */
|
||||||
|
public static final String PROPERTY_REGISTRIES = "aere.pq.registryV3";
|
||||||
|
|
||||||
|
/** The pinned hashes of the v3 manifests. */
|
||||||
|
public static final String PROPERTY_HASHES = "aere.pq.registryV3.hashes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AIP-22 stage 3b: H_k, the height from which the keys that SEAL come from the v3 epoch in force
|
||||||
|
* (instead of the Falcon registry schedule and the hybrid registry). Must be the bindHeight of a v3
|
||||||
|
* epoch and at or above H_id. Unset: the keys stay where they are (stage 3a).
|
||||||
|
*/
|
||||||
|
public static final String PROPERTY_KEYS_FROM = "aere.pq.registryV3.keysFrom";
|
||||||
|
|
||||||
|
/** No H_k: the v3 epochs never supply keys. */
|
||||||
|
public static final long NEVER = Long.MAX_VALUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An installed configuration.
|
||||||
|
*
|
||||||
|
* @param identityBlock H_id
|
||||||
|
* @param epochs v3 epochs by bindHeight
|
||||||
|
* @param keysFrom H_k, or {@link #NEVER}
|
||||||
|
*/
|
||||||
|
public record Config(
|
||||||
|
long identityBlock, NavigableMap<Long, PqRegistryV3.Registry> epochs, long keysFrom) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stage-3a configuration: identity only, keys stay in the existing registries.
|
||||||
|
*
|
||||||
|
* @param identityBlock H_id
|
||||||
|
* @param epochs v3 epochs by bindHeight
|
||||||
|
*/
|
||||||
|
public Config(final long identityBlock, final NavigableMap<Long, PqRegistryV3.Registry> epochs) {
|
||||||
|
this(identityBlock, epochs, NEVER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A configuration fault of the identity switch. */
|
||||||
|
public static final class IdentityException extends RuntimeException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fault.
|
||||||
|
*
|
||||||
|
* @param message what is wrong
|
||||||
|
*/
|
||||||
|
public IdentityException(final String message) {
|
||||||
|
super("AERE-PQC-ID: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static volatile Config config;
|
||||||
|
|
||||||
|
private AerePqIdentity() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a configuration (startup, or a test). Checks that H_id has an epoch bound at or below it.
|
||||||
|
*
|
||||||
|
* @param c the configuration, or null to disarm
|
||||||
|
*/
|
||||||
|
public static void install(final Config c) {
|
||||||
|
if (c != null && c.epochs().floorEntry(c.identityBlock()) == null) {
|
||||||
|
throw new IdentityException(
|
||||||
|
"no v3 registry epoch is bound at or below the identity height " + c.identityBlock());
|
||||||
|
}
|
||||||
|
if (c != null && c.keysFrom() != NEVER) {
|
||||||
|
// H_k switches where the keys come FROM; it must fall on an epoch boundary at or after H_id, so
|
||||||
|
// the epoch that starts supplying keys is a whole epoch, verified and pinned like every other
|
||||||
|
if (c.keysFrom() < c.identityBlock() || !c.epochs().containsKey(c.keysFrom())) {
|
||||||
|
throw new IdentityException(
|
||||||
|
PROPERTY_KEYS_FROM
|
||||||
|
+ "="
|
||||||
|
+ c.keysFrom()
|
||||||
|
+ " must be the bindHeight of a v3 epoch at or above the identity height "
|
||||||
|
+ c.identityBlock()
|
||||||
|
+ " (epochs "
|
||||||
|
+ c.epochs().keySet()
|
||||||
|
+ ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config = c;
|
||||||
|
if (c != null) {
|
||||||
|
LOG.info(
|
||||||
|
"AERE PQ IDENTITY: ARMED - validators are post-quantum ids from height {}; v3 epochs {}{}",
|
||||||
|
c.identityBlock(),
|
||||||
|
c.epochs().keySet(),
|
||||||
|
c.keysFrom() == NEVER
|
||||||
|
? ""
|
||||||
|
: "; the keys that seal come from the v3 epoch in force from height " + c.keysFrom());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Disarms (tests). */
|
||||||
|
public static void uninstall() {
|
||||||
|
config = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The installed configuration.
|
||||||
|
*
|
||||||
|
* @return it, if armed
|
||||||
|
*/
|
||||||
|
public static Optional<Config> installed() {
|
||||||
|
return Optional.ofNullable(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the configuration from the system properties and installs it. No property: stays disarmed.
|
||||||
|
*
|
||||||
|
* @param chainId the chain
|
||||||
|
* @param requiredAt the schemes every row must carry, as the scheme schedule names them at a height
|
||||||
|
*/
|
||||||
|
public static void installFromSystemProperties(
|
||||||
|
final long chainId, final java.util.function.LongFunction<List<String>> requiredAt) {
|
||||||
|
final String block = System.getProperty(PROPERTY_BLOCK);
|
||||||
|
if (block == null || block.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final long h = Long.parseLong(block.trim());
|
||||||
|
final List<String> requiredSchemes = requiredAt.apply(h);
|
||||||
|
final String files = System.getProperty(PROPERTY_REGISTRIES, "");
|
||||||
|
final String hashes = System.getProperty(PROPERTY_HASHES, "");
|
||||||
|
final TreeMap<Long, Bytes32> pins = new TreeMap<>();
|
||||||
|
for (final String p : Splitter.on(',').omitEmptyStrings().trimResults().split(hashes)) {
|
||||||
|
final int i = p.indexOf(':');
|
||||||
|
if (i < 0) {
|
||||||
|
throw new IdentityException(PROPERTY_HASHES + " entry '" + p + "' is not bindHeight:0xhash");
|
||||||
|
}
|
||||||
|
pins.put(Long.parseLong(p.substring(0, i)), Bytes32.fromHexString(p.substring(i + 1)));
|
||||||
|
}
|
||||||
|
final ObjectMapper m = new ObjectMapper();
|
||||||
|
final List<PqRegistryV3.Registry> loaded = new ArrayList<>();
|
||||||
|
PqRegistryV3.Registry previous = null;
|
||||||
|
final List<Path> paths = new ArrayList<>();
|
||||||
|
for (final String f : Splitter.on(',').omitEmptyStrings().trimResults().split(files)) {
|
||||||
|
paths.add(Path.of(f));
|
||||||
|
}
|
||||||
|
// epochs are loaded in bindHeight order, each against the one before it (rotations)
|
||||||
|
final TreeMap<Long, com.fasterxml.jackson.databind.JsonNode> byHeight = new TreeMap<>();
|
||||||
|
for (final Path p : paths) {
|
||||||
|
try {
|
||||||
|
final com.fasterxml.jackson.databind.JsonNode j = m.readTree(Files.readString(p));
|
||||||
|
byHeight.put(j.path("bindHeight").asLong(-1), j);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new IdentityException("cannot read v3 manifest " + p + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final var e : byHeight.entrySet()) {
|
||||||
|
final Bytes32 pin = pins.get(e.getKey());
|
||||||
|
if (pin == null) {
|
||||||
|
throw new IdentityException(
|
||||||
|
"the v3 epoch bound at " + e.getKey() + " has no pinned hash in " + PROPERTY_HASHES);
|
||||||
|
}
|
||||||
|
final boolean switchEpoch = previous == null;
|
||||||
|
final PqRegistryV3.Registry r =
|
||||||
|
PqRegistryV3.loadPinned(e.getValue(), chainId, requiredSchemes, previous, switchEpoch, pin);
|
||||||
|
loaded.add(r);
|
||||||
|
previous = r;
|
||||||
|
}
|
||||||
|
final TreeMap<Long, PqRegistryV3.Registry> epochs = new TreeMap<>();
|
||||||
|
for (final PqRegistryV3.Registry r : loaded) {
|
||||||
|
epochs.put(r.bindHeight(), r);
|
||||||
|
}
|
||||||
|
final String keysFrom = System.getProperty(PROPERTY_KEYS_FROM);
|
||||||
|
install(
|
||||||
|
new Config(
|
||||||
|
h,
|
||||||
|
Collections.unmodifiableNavigableMap(epochs),
|
||||||
|
keysFrom == null || keysFrom.isBlank() ? NEVER : Long.parseLong(keysFrom.trim())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// AIP-22 stage 3b: the keys that seal, from the v3 epoch in force
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the keys that seal at this height come from the v3 epoch in force (at or above H_k).
|
||||||
|
* Every key lookup asks this first; below H_k (and disarmed) it answers false and the caller uses
|
||||||
|
* its existing registry, untouched.
|
||||||
|
*
|
||||||
|
* @param height the height the key is asked for, in the caller's own convention (the parent of an
|
||||||
|
* anchor for a certificate seal)
|
||||||
|
* @return true from H_k when armed
|
||||||
|
*/
|
||||||
|
public static boolean keysFromV3(final long height) {
|
||||||
|
final Config c = config;
|
||||||
|
return c != null && height >= c.keysFrom();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The public key of a registry index for a scheme at a height, from the v3 epoch in force, in the
|
||||||
|
* form the existing registries carry it: Falcon-512 WITHOUT the 0x09 header byte (the 896-byte h
|
||||||
|
* that {@code FalconPublicKeyParameters} takes), SLH-DSA-SHA2-128s as its 32 bytes. Empty when the
|
||||||
|
* epoch has no such row or key: the caller then has NO key (fail closed), it never falls back to
|
||||||
|
* the old registries, because above H_k they are no longer the key set the chain requires.
|
||||||
|
*
|
||||||
|
* @param height the height (see {@link #keysFromV3})
|
||||||
|
* @param index the registry index carried by the seal
|
||||||
|
* @param schemeId e.g. "falcon-512"
|
||||||
|
* @return the key, or empty
|
||||||
|
*/
|
||||||
|
public static Optional<byte[]> v3KeyAt(final long height, final int index, final String schemeId) {
|
||||||
|
return epochAt(height)
|
||||||
|
.filter(r -> index >= 0 && index < r.rows().size())
|
||||||
|
.map(r -> r.rows().get(index))
|
||||||
|
.flatMap(row -> row.key(schemeId))
|
||||||
|
.map(
|
||||||
|
k ->
|
||||||
|
"falcon-512".equals(schemeId)
|
||||||
|
? java.util.Arrays.copyOfRange(k.publicKey(), 1, k.publicKey().length)
|
||||||
|
: k.publicKey().clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AIP-22 stage 3b-ii: the index of the row of the v3 epoch in force that carries a given public key
|
||||||
|
* for a scheme (the key in the registry form of {@link #v3KeyAt}). How a node finds, per epoch,
|
||||||
|
* which row is its own: indices move between epochs, so a key file's index cannot say it.
|
||||||
|
*
|
||||||
|
* @param height the height
|
||||||
|
* @param schemeId e.g. "falcon-512"
|
||||||
|
* @param publicKey the key, registry form
|
||||||
|
* @return the row index, or empty when no row of that epoch carries the key
|
||||||
|
*/
|
||||||
|
public static Optional<Integer> v3IndexOfKey(
|
||||||
|
final long height, final String schemeId, final byte[] publicKey) {
|
||||||
|
final Optional<PqRegistryV3.Registry> r = epochAt(height);
|
||||||
|
if (r.isEmpty() || publicKey == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
for (final PqRegistryV3.Row row : r.get().rows()) {
|
||||||
|
final Optional<byte[]> k = v3KeyAt(height, row.index(), schemeId);
|
||||||
|
if (k.isPresent() && java.util.Arrays.equals(k.get(), publicKey)) {
|
||||||
|
return Optional.of(row.index());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of a registry index at a height, from the v3 epoch in force (stage 3b: the index names
|
||||||
|
* the v3 row directly, from H_k).
|
||||||
|
*
|
||||||
|
* @param height the height
|
||||||
|
* @param index the registry index
|
||||||
|
* @return the 20-byte id as an address, or empty when the epoch has no such row
|
||||||
|
*/
|
||||||
|
public static Optional<Address> v3IdAt(final long height, final int index) {
|
||||||
|
return epochAt(height)
|
||||||
|
.filter(r -> index >= 0 && index < r.rows().size())
|
||||||
|
.map(r -> Address.wrap(Bytes.wrap(r.rows().get(index).id())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE CONTINUITY GUARD, run once at startup when H_k is configured: switching the SOURCE of the
|
||||||
|
* keys at H_k must not change any key. Every key of every row of the epoch in force at H_k must be
|
||||||
|
* exactly the key the existing registries give that index just below H_k; only a LATER epoch may
|
||||||
|
* rotate, and only through the rotation signatures of the old keys, which the v3 loader verifies.
|
||||||
|
* A mismatch, or a key the existing registries cannot produce, refuses startup (fail closed): a
|
||||||
|
* node that swapped keys at H_k would accept seals no other node accepts.
|
||||||
|
*
|
||||||
|
* @param keysBefore (index, schemeId) to the key the existing registries give at H_k - 1, in the
|
||||||
|
* registry form (see {@link #v3KeyAt})
|
||||||
|
*/
|
||||||
|
public static void verifyKeyContinuity(
|
||||||
|
final java.util.function.BiFunction<Integer, String, Optional<byte[]>> keysBefore) {
|
||||||
|
final Config c = config;
|
||||||
|
if (c == null || c.keysFrom() == NEVER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final PqRegistryV3.Registry r = c.epochs().get(c.keysFrom());
|
||||||
|
int checked = 0;
|
||||||
|
for (final PqRegistryV3.Row row : r.rows()) {
|
||||||
|
for (final PqRegistryV3.Key k : row.keys().values()) {
|
||||||
|
final String scheme = k.scheme().id();
|
||||||
|
final byte[] v3 = v3KeyAt(c.keysFrom(), row.index(), scheme).orElseThrow();
|
||||||
|
final Optional<byte[]> before = keysBefore.apply(row.index(), scheme);
|
||||||
|
if (before.isEmpty() || !java.util.Arrays.equals(before.get(), v3)) {
|
||||||
|
throw new IdentityException(
|
||||||
|
"continuity at "
|
||||||
|
+ PROPERTY_KEYS_FROM
|
||||||
|
+ "="
|
||||||
|
+ c.keysFrom()
|
||||||
|
+ ": row "
|
||||||
|
+ row.index()
|
||||||
|
+ " "
|
||||||
|
+ scheme
|
||||||
|
+ (before.isEmpty()
|
||||||
|
? " has no key in the existing registries at " + (c.keysFrom() - 1)
|
||||||
|
: " differs from the key the existing registries give at " + (c.keysFrom() - 1))
|
||||||
|
+ "; switching the key source must not change a key (a rotation belongs in a later"
|
||||||
|
+ " epoch, signed by the old keys)");
|
||||||
|
}
|
||||||
|
checked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG.info(
|
||||||
|
"AERE PQ IDENTITY: key continuity at {} verified, {} keys of {} rows equal the existing"
|
||||||
|
+ " registries at {}",
|
||||||
|
c.keysFrom(),
|
||||||
|
checked,
|
||||||
|
r.rows().size(),
|
||||||
|
c.keysFrom() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether ids are the identity at this height.
|
||||||
|
*
|
||||||
|
* @param height the height
|
||||||
|
* @return true from H_id when armed
|
||||||
|
*/
|
||||||
|
public static boolean active(final long height) {
|
||||||
|
final Config c = config;
|
||||||
|
return c != null && height >= c.identityBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The v3 epoch in force at a height.
|
||||||
|
*
|
||||||
|
* @param height the height
|
||||||
|
* @return the epoch, if armed and one is bound at or below it
|
||||||
|
*/
|
||||||
|
public static Optional<PqRegistryV3.Registry> epochAt(final long height) {
|
||||||
|
final Config c = config;
|
||||||
|
if (c == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
final var e = c.epochs().floorEntry(height);
|
||||||
|
return e == null ? Optional.empty() : Optional.of(e.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The author of a message or commit seal whose ECDSA signature recovered to this address.
|
||||||
|
*
|
||||||
|
* @param recovered the recovered address
|
||||||
|
* @param height the height the message or seal is for
|
||||||
|
* @return the id from H_id (when a row links the address), the address otherwise
|
||||||
|
*/
|
||||||
|
public static Address author(final Address recovered, final long height) {
|
||||||
|
if (!active(height)) {
|
||||||
|
return recovered;
|
||||||
|
}
|
||||||
|
return idOfEcdsa(recovered.getBytes().toArrayUnsafe(), height)
|
||||||
|
.map(i -> Address.wrap(Bytes.wrap(i)))
|
||||||
|
.orElse(recovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id an ECDSA address links to at a height. The ecdsa link is required only in the switch epoch
|
||||||
|
* (AIP-22 3.2) and a later epoch may omit it, while messages still travel with an ECDSA signature
|
||||||
|
* until stage 4; so the link is INHERITED by id: the most recent epoch at or below the height that
|
||||||
|
* links this address names the id, and that id must still be registered in the epoch in force (a
|
||||||
|
* removed id's old link means nothing). A new id that never carried a link has no ECDSA author, so
|
||||||
|
* its classical messages cannot count before stage 4: stated in the plan (9.5), not hidden.
|
||||||
|
*/
|
||||||
|
private static Optional<byte[]> idOfEcdsa(final byte[] address, final long height) {
|
||||||
|
final Config c = config;
|
||||||
|
if (c == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
final var head = c.epochs().floorEntry(height);
|
||||||
|
if (head == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
// ids that a NEWER epoch already links to some address: an older link of another address to such an
|
||||||
|
// id is superseded (the id moved its ECDSA key), so that older address no longer authors as the id
|
||||||
|
final java.util.Set<Bytes> relinked = new java.util.HashSet<>();
|
||||||
|
for (final PqRegistryV3.Registry e : c.epochs().headMap(height, true).descendingMap().values()) {
|
||||||
|
final Optional<PqRegistryV3.Row> linked = e.rowOfEcdsa(address);
|
||||||
|
if (linked.isPresent()) {
|
||||||
|
final byte[] i = linked.get().id();
|
||||||
|
return !relinked.contains(Bytes.wrap(i)) && head.getValue().rowOf(i).isPresent()
|
||||||
|
? Optional.of(i)
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
for (final PqRegistryV3.Row r : e.rows()) {
|
||||||
|
if (r.ecdsaAddress() != null) {
|
||||||
|
relinked.add(Bytes.wrap(r.id()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This node's identity at a height.
|
||||||
|
*
|
||||||
|
* @param localEcdsa the node's ECDSA address
|
||||||
|
* @param height the height
|
||||||
|
* @return the id from H_id when the node has a row, the address otherwise
|
||||||
|
*/
|
||||||
|
public static Address local(final Address localEcdsa, final long height) {
|
||||||
|
return author(localEcdsa, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a 20-byte value is this node, as an address or as its id in any epoch.
|
||||||
|
*
|
||||||
|
* @param candidate the value
|
||||||
|
* @param localEcdsa the node's ECDSA address
|
||||||
|
* @return whether it is this node
|
||||||
|
*/
|
||||||
|
public static boolean isLocal(final Address candidate, final Address localEcdsa) {
|
||||||
|
if (candidate.equals(localEcdsa)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final Config c = config;
|
||||||
|
if (c == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (final PqRegistryV3.Registry r : c.epochs().values()) {
|
||||||
|
final Optional<PqRegistryV3.Row> row = r.rowOfEcdsa(localEcdsa.getBytes().toArrayUnsafe());
|
||||||
|
if (row.isPresent() && candidate.equals(Address.wrap(Bytes.wrap(row.get().id())))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the set after this block changes form (armed, and the block is H_id - 1). Callers ask
|
||||||
|
* first so that, disarmed or at any other block, the upstream code path is untouched - not even an
|
||||||
|
* extra call on the tally (upstream VoteTallyUpdaterTest asserts exactly that).
|
||||||
|
*
|
||||||
|
* @param blockNumber the block
|
||||||
|
* @return true only at H_id - 1 when armed
|
||||||
|
*/
|
||||||
|
public static boolean translatesAfter(final long blockNumber) {
|
||||||
|
final Config c = config;
|
||||||
|
return c != null && blockNumber == c.identityBlock() - 1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The validator set after block {@code blockNumber}, translated when that block is H_id - 1.
|
||||||
|
*
|
||||||
|
* @param set the set after the block, as upstream computed it
|
||||||
|
* @param blockNumber the block
|
||||||
|
* @return the same set, or its sorted ids at H_id - 1
|
||||||
|
*/
|
||||||
|
public static Collection<Address> translateSetAfter(
|
||||||
|
final Collection<Address> set, final long blockNumber) {
|
||||||
|
final Config c = config;
|
||||||
|
if (c == null || blockNumber != c.identityBlock() - 1L) {
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
final PqRegistryV3.Registry r =
|
||||||
|
epochAt(c.identityBlock())
|
||||||
|
.orElseThrow(() -> new IdentityException("no v3 epoch at " + c.identityBlock()));
|
||||||
|
final TreeSet<Address> ids = new TreeSet<>();
|
||||||
|
for (final Address a : set) {
|
||||||
|
final Optional<PqRegistryV3.Row> row = r.rowOfEcdsa(a.getBytes().toArrayUnsafe());
|
||||||
|
if (row.isEmpty()) {
|
||||||
|
throw new IdentityException(
|
||||||
|
"validator "
|
||||||
|
+ a
|
||||||
|
+ " is in the set after block "
|
||||||
|
+ blockNumber
|
||||||
|
+ " and has no row in the v3 epoch bound at "
|
||||||
|
+ r.bindHeight()
|
||||||
|
+ "; the identity switch refuses rather than dropping a validator");
|
||||||
|
}
|
||||||
|
ids.add(Address.wrap(Bytes.wrap(row.get().id())));
|
||||||
|
}
|
||||||
|
LOG.info(
|
||||||
|
"AERE PQ IDENTITY: the set after block {} is translated to {} post-quantum ids",
|
||||||
|
blockNumber,
|
||||||
|
ids.size());
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RLPx node-key addresses consensus messages for these recipients go to.
|
||||||
|
*
|
||||||
|
* @param recipients validator identities (ids or addresses)
|
||||||
|
* @return the transport addresses; EMPTY when some id lists no transport, meaning "every peer"
|
||||||
|
*/
|
||||||
|
public static Optional<Collection<Address>> transportTargets(final Collection<Address> recipients) {
|
||||||
|
final Config c = config;
|
||||||
|
if (c == null) {
|
||||||
|
return Optional.of(recipients);
|
||||||
|
}
|
||||||
|
final PqRegistryV3.Registry head = c.epochs().lastEntry().getValue();
|
||||||
|
final List<Address> out = new ArrayList<>();
|
||||||
|
for (final Address a : recipients) {
|
||||||
|
final Optional<PqRegistryV3.Row> row = head.rowOf(a.getBytes().toArrayUnsafe());
|
||||||
|
if (row.isEmpty()) {
|
||||||
|
out.add(a); // a classical address (below H_id), sent to as before
|
||||||
|
} else if (row.get().transport() == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
out.add(Address.wrap(Bytes.wrap(row.get().transport())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.of(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -156,7 +156,7 @@ public class BftBlockInterface implements BlockInterface {
|
|||||||
bftExtraDataCodec));
|
bftExtraDataCodec));
|
||||||
|
|
||||||
return bftExtraData.getSeals().stream()
|
return bftExtraData.getSeals().stream()
|
||||||
.map(p -> Util.signatureToAddress(p, committerHash))
|
.map(p -> AerePqIdentity.author(Util.signatureToAddress(p, committerHash), header.getNumber()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -151,6 +151,27 @@ public final class FalconSealSupport {
|
|||||||
private final boolean signingEnabled;
|
private final boolean signingEnabled;
|
||||||
private final int localIndex;
|
private final int localIndex;
|
||||||
private final FalconPrivateKeyParameters localPrivateKey;
|
private final FalconPrivateKeyParameters localPrivateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE AIP-22 stage 3b-ii: this node's NEXT Falcon keys ({@code aere.falcon.keys.next} /
|
||||||
|
* {@code AERE_FALCON_KEYS_NEXT}, comma list of key files in the same format as {@code
|
||||||
|
* aere.falcon.key}), for the v3 epochs that rotate its key. From H_k the key and the index this node
|
||||||
|
* signs with at a height are the ones of the v3 row that carries one of its keys (see {@link
|
||||||
|
* #localAt}); the file's "index" property then means nothing, because indices move between epochs.
|
||||||
|
*/
|
||||||
|
private final java.util.List<FalconPrivateKeyParameters> nextPrivateKeys;
|
||||||
|
|
||||||
|
/** The local key and index resolved per v3 epoch (bindHeight), from H_k. */
|
||||||
|
private final Map<Long, java.util.Optional<LocalKey>> localByEpoch = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A local signing key with the registry index it signs as.
|
||||||
|
*
|
||||||
|
* @param key the private key
|
||||||
|
* @param index the registry index at the height it was resolved for
|
||||||
|
*/
|
||||||
|
private record LocalKey(FalconPrivateKeyParameters key, int index) {}
|
||||||
|
|
||||||
private final Map<Integer, FalconPublicKeyParameters> registry;
|
private final Map<Integer, FalconPublicKeyParameters> registry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -590,6 +611,36 @@ public final class FalconSealSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AERE AIP-22 stage 3b-ii: the next keys, same file format; a file that does not load refuses
|
||||||
|
// startup, because a validator that silently lacks its rotated key goes mute at the rotation epoch
|
||||||
|
final java.util.List<FalconPrivateKeyParameters> next = new java.util.ArrayList<>();
|
||||||
|
final String nextPaths = resolve("aere.falcon.keys.next", "AERE_FALCON_KEYS_NEXT");
|
||||||
|
if (nextPaths != null) {
|
||||||
|
for (final String np : com.google.common.base.Splitter.on(',').omitEmptyStrings().trimResults().split(nextPaths)) {
|
||||||
|
if (np.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try (InputStream in = new FileInputStream(np.trim())) {
|
||||||
|
final Properties p = new Properties();
|
||||||
|
p.load(in);
|
||||||
|
next.add(
|
||||||
|
new FalconPrivateKeyParameters(
|
||||||
|
FalconParameters.falcon_512,
|
||||||
|
Bytes.fromHexStringLenient(p.getProperty("f").trim()).toArray(),
|
||||||
|
Bytes.fromHexStringLenient(p.getProperty("g").trim()).toArray(),
|
||||||
|
Bytes.fromHexStringLenient(p.getProperty("F").trim()).toArray(),
|
||||||
|
Bytes.fromHexStringLenient(p.getProperty("pk").trim()).toArray()));
|
||||||
|
LOG.info("AERE PQC: loaded a NEXT Falcon signing key from {} (AIP-22 stage 3b-ii)", np.trim());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"AERE-PQC-KEY-NEXT-01: cannot load the next Falcon key from '"
|
||||||
|
+ np.trim()
|
||||||
|
+ "': "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nextPrivateKeys = java.util.List.copyOf(next);
|
||||||
this.registry = reg;
|
this.registry = reg;
|
||||||
this.indexToAddress = regAddr;
|
this.indexToAddress = regAddr;
|
||||||
this.localIndex = idx;
|
this.localIndex = idx;
|
||||||
@ -2524,6 +2575,89 @@ public final class FalconSealSupport {
|
|||||||
return signingEnabled;
|
return signingEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE AIP-22 stage 3b-ii. The key this node signs with at a height, and the index it signs as.
|
||||||
|
* Below H_k (and disarmed): the key of aere.falcon.key and its file index, exactly as before. From
|
||||||
|
* H_k: the v3 row of the epoch in force whose Falcon key is the public key of one of this node's
|
||||||
|
* keys (the base key or a next key); indices move between epochs, so the index is that row's. No
|
||||||
|
* row carries any of this node's keys: null, so nothing is signed (the startup guard {@link
|
||||||
|
* #verifyLocalKeysCoverV3EpochsOrAbort} refuses a validator that would end up there).
|
||||||
|
*/
|
||||||
|
private LocalKey localAt(final long height) {
|
||||||
|
if (!AerePqIdentity.keysFromV3(height)) {
|
||||||
|
return localPrivateKey == null ? null : new LocalKey(localPrivateKey, localIndex);
|
||||||
|
}
|
||||||
|
final long bind =
|
||||||
|
AerePqIdentity.epochAt(height).map(PqRegistryV3.Registry::bindHeight).orElse(-1L);
|
||||||
|
return localByEpoch
|
||||||
|
.computeIfAbsent(
|
||||||
|
bind,
|
||||||
|
b -> {
|
||||||
|
final java.util.List<FalconPrivateKeyParameters> mine = new java.util.ArrayList<>();
|
||||||
|
if (localPrivateKey != null) {
|
||||||
|
mine.add(localPrivateKey);
|
||||||
|
}
|
||||||
|
mine.addAll(nextPrivateKeys);
|
||||||
|
for (final FalconPrivateKeyParameters k : mine) {
|
||||||
|
final java.util.Optional<Integer> i =
|
||||||
|
AerePqIdentity.v3IndexOfKey(height, "falcon-512", k.getPublicKey());
|
||||||
|
if (i.isPresent()) {
|
||||||
|
return java.util.Optional.of(new LocalKey(k, i.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return java.util.Optional.empty();
|
||||||
|
})
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE AIP-22 stage 3b-ii. The registry index this node signs as at a height (the index of every
|
||||||
|
* other scheme's seal of this node too: an SLH-DSA seal must sit at the index of its Falcon seal).
|
||||||
|
*
|
||||||
|
* @param height the height
|
||||||
|
* @return the index, or -1 when this node holds no key of that height
|
||||||
|
*/
|
||||||
|
public int localIndexAt(final long height) {
|
||||||
|
final LocalKey lk = localAt(height);
|
||||||
|
return lk == null ? -1 : lk.index();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE AIP-22 stage 3b-ii, THE STARTUP GUARD of the local keys: in every v3 epoch from H_k in which
|
||||||
|
* this node's identity is registered, this node must hold the Falcon key of its row, otherwise it
|
||||||
|
* refuses to start. A validator whose rotated key is missing would sign nothing from the rotation
|
||||||
|
* epoch on: mute, while every gate that asks "is it configured?" stays green (D-307).
|
||||||
|
*
|
||||||
|
* @param thisNodeAddress this node's own validator (ECDSA) address
|
||||||
|
*/
|
||||||
|
public void verifyLocalKeysCoverV3EpochsOrAbort(final Address thisNodeAddress) {
|
||||||
|
final java.util.Optional<AerePqIdentity.Config> c = AerePqIdentity.installed();
|
||||||
|
if (c.isEmpty() || c.get().keysFrom() == AerePqIdentity.NEVER || thisNodeAddress == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (final PqRegistryV3.Registry e : c.get().epochs().tailMap(c.get().keysFrom(), true).values()) {
|
||||||
|
final Address me = AerePqIdentity.author(thisNodeAddress, e.bindHeight());
|
||||||
|
if (e.rowOf(me.getBytes().toArrayUnsafe()).isEmpty()) {
|
||||||
|
continue; // this node is not a registered validator in this epoch: nothing to sign
|
||||||
|
}
|
||||||
|
if (localAt(e.bindHeight()) == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"AERE-PQC-KEY-NEXT-02: REFUSING TO START. This node ("
|
||||||
|
+ thisNodeAddress
|
||||||
|
+ ", id "
|
||||||
|
+ me
|
||||||
|
+ ") is registered in the v3 epoch bound at "
|
||||||
|
+ e.bindHeight()
|
||||||
|
+ " but holds no Falcon key of its row there (aere.falcon.key / aere.falcon.keys.next);"
|
||||||
|
+ " from that height it would sign nothing.");
|
||||||
|
}
|
||||||
|
LOG.info(
|
||||||
|
"AERE PQC: local Falcon key covers the v3 epoch bound at {} (index {}).",
|
||||||
|
e.bindHeight(),
|
||||||
|
localAt(e.bindHeight()).index());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This node's validator index in the Falcon registry.
|
* This node's validator index in the Falcon registry.
|
||||||
*
|
*
|
||||||
@ -3460,13 +3594,17 @@ public final class FalconSealSupport {
|
|||||||
// AERE AGILITY step 4 (2026-08-24): signing goes through the scheme layer. The same
|
// AERE AGILITY step 4 (2026-08-24): signing goes through the scheme layer. The same
|
||||||
// FalconSigner underneath, but the path is now the one the hybrid will use too; the 751
|
// FalconSigner underneath, but the path is now the one the hybrid will use too; the 751
|
||||||
// baseline tests prove the equivalence.
|
// baseline tests prove the equivalence.
|
||||||
|
final LocalKey lk = localAt(blockNumber); // AERE AIP-22 stage 3b-ii
|
||||||
|
if (lk == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
final java.util.Optional<byte[]> sig =
|
final java.util.Optional<byte[]> sig =
|
||||||
((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(localPrivateKey, commitHash.toArray());
|
((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(lk.key(), commitHash.toArray());
|
||||||
if (sig.isEmpty()) {
|
if (sig.isEmpty()) {
|
||||||
LOG.warn("AERE PQC: Falcon signing failed (ECDSA seal unaffected)");
|
LOG.warn("AERE PQC: Falcon signing failed (ECDSA seal unaffected)");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
return Optional.of(new FalconSeal(localIndex, Bytes.wrap(sig.get())));
|
return Optional.of(new FalconSeal(lk.index(), Bytes.wrap(sig.get())));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3548,8 +3686,12 @@ public final class FalconSealSupport {
|
|||||||
if (blockNumber < prepareAttachBlock()) {
|
if (blockNumber < prepareAttachBlock()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
final LocalKey lk = localAt(blockNumber); // AERE AIP-22 stage 3b-ii
|
||||||
|
if (lk == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
final java.util.Optional<byte[]> sig =
|
final java.util.Optional<byte[]> sig =
|
||||||
((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(localPrivateKey, message.toArray());
|
((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(lk.key(), message.toArray());
|
||||||
if (sig.isEmpty()) {
|
if (sig.isEmpty()) {
|
||||||
LOG.warn("AERE PQ PREPARE: Falcon signing failed (ECDSA path unaffected)");
|
LOG.warn("AERE PQ PREPARE: Falcon signing failed (ECDSA path unaffected)");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@ -3570,7 +3712,7 @@ public final class FalconSealSupport {
|
|||||||
} else if (n % 500L == 0L) {
|
} else if (n % 500L == 0L) {
|
||||||
LOG.info("AERE PQ PREPARE: {} seals emitted on PREPAREs since startup.", n);
|
LOG.info("AERE PQ PREPARE: {} seals emitted on PREPAREs since startup.", n);
|
||||||
}
|
}
|
||||||
return Optional.of(new FalconSeal(localIndex, Bytes.wrap(sig.get())));
|
return Optional.of(new FalconSeal(lk.index(), Bytes.wrap(sig.get())));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3653,8 +3795,12 @@ public final class FalconSealSupport {
|
|||||||
if (blockNumber < proposalAttachBlock()) {
|
if (blockNumber < proposalAttachBlock()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
final LocalKey lk = localAt(blockNumber); // AERE AIP-22 stage 3b-ii
|
||||||
|
if (lk == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
final java.util.Optional<byte[]> sig =
|
final java.util.Optional<byte[]> sig =
|
||||||
((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(localPrivateKey, message.toArray());
|
((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(lk.key(), message.toArray());
|
||||||
if (sig.isEmpty()) {
|
if (sig.isEmpty()) {
|
||||||
LOG.warn("AERE PQ PROPOSAL: Falcon signing failed (ECDSA path unaffected)");
|
LOG.warn("AERE PQ PROPOSAL: Falcon signing failed (ECDSA path unaffected)");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@ -3672,7 +3818,7 @@ public final class FalconSealSupport {
|
|||||||
} else if (n % 500L == 0L) {
|
} else if (n % 500L == 0L) {
|
||||||
LOG.info("AERE PQ PROPOSAL: {} seals emitted on PROPOSALs since startup.", n);
|
LOG.info("AERE PQ PROPOSAL: {} seals emitted on PROPOSALs since startup.", n);
|
||||||
}
|
}
|
||||||
return Optional.of(new FalconSeal(localIndex, Bytes.wrap(sig.get())));
|
return Optional.of(new FalconSeal(lk.index(), Bytes.wrap(sig.get())));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3752,8 +3898,12 @@ public final class FalconSealSupport {
|
|||||||
if (blockNumber < roundChangeAttachBlock()) {
|
if (blockNumber < roundChangeAttachBlock()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
final LocalKey lk = localAt(blockNumber); // AERE AIP-22 stage 3b-ii
|
||||||
|
if (lk == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
final java.util.Optional<byte[]> sig =
|
final java.util.Optional<byte[]> sig =
|
||||||
((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(localPrivateKey, message.toArray());
|
((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(lk.key(), message.toArray());
|
||||||
if (sig.isEmpty()) {
|
if (sig.isEmpty()) {
|
||||||
LOG.warn("AERE PQ ROUNDCHANGE: Falcon signing failed (ECDSA path unaffected)");
|
LOG.warn("AERE PQ ROUNDCHANGE: Falcon signing failed (ECDSA path unaffected)");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@ -3771,7 +3921,7 @@ public final class FalconSealSupport {
|
|||||||
} else if (n % 100L == 0L) {
|
} else if (n % 100L == 0L) {
|
||||||
LOG.info("AERE PQ ROUNDCHANGE: {} seals emitted on ROUND-CHANGEs since startup.", n);
|
LOG.info("AERE PQ ROUNDCHANGE: {} seals emitted on ROUND-CHANGEs since startup.", n);
|
||||||
}
|
}
|
||||||
return Optional.of(new FalconSeal(localIndex, Bytes.wrap(sig.get())));
|
return Optional.of(new FalconSeal(lk.index(), Bytes.wrap(sig.get())));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3813,6 +3963,15 @@ public final class FalconSealSupport {
|
|||||||
*/
|
*/
|
||||||
private FalconPublicKeyParameters keyAt(
|
private FalconPublicKeyParameters keyAt(
|
||||||
final long blockNumber, final int validatorIndex, final boolean historic) {
|
final long blockNumber, final int validatorIndex, final boolean historic) {
|
||||||
|
// AERE AIP-22 stage 3b: from H_k the key of an index is the one of the v3 epoch in force at this
|
||||||
|
// SAME height (the caller's convention is kept: the parent of an anchor for its certificate). No
|
||||||
|
// row or no key there means no key: never a fall-back to the registries below, which above H_k
|
||||||
|
// are no longer the key set the chain requires. Below H_k (and disarmed) nothing changes.
|
||||||
|
if (AerePqIdentity.keysFromV3(blockNumber)) {
|
||||||
|
return AerePqIdentity.v3KeyAt(blockNumber, validatorIndex, "falcon-512")
|
||||||
|
.map(pk -> new FalconPublicKeyParameters(FalconParameters.falcon_512, pk))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
final PqRegistryHash.Schedule schedule = this.registryBindingSchedule;
|
final PqRegistryHash.Schedule schedule = this.registryBindingSchedule;
|
||||||
final PqRegistryHash.RegistrySet set = this.registryBindingSet;
|
final PqRegistryHash.RegistrySet set = this.registryBindingSet;
|
||||||
if (schedule == null || set == null) {
|
if (schedule == null || set == null) {
|
||||||
@ -3868,6 +4027,20 @@ public final class FalconSealSupport {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE AIP-22 stage 3b. The Falcon-512 public key (the 896-byte h, as the registries carry it) that
|
||||||
|
* the chain's registries give an index at a height, through the same history door as a received
|
||||||
|
* header. Used by the continuity guard at H_k - 1, where the v3 epochs do not yet supply keys.
|
||||||
|
*
|
||||||
|
* @param blockNumber the height
|
||||||
|
* @param validatorIndex the registry index
|
||||||
|
* @return the key, or empty when the registries give none there
|
||||||
|
*/
|
||||||
|
public Optional<byte[]> publicKeyBytesAtHistoric(final long blockNumber, final int validatorIndex) {
|
||||||
|
final FalconPublicKeyParameters k = keyAt(blockNumber, validatorIndex, true);
|
||||||
|
return k == null ? Optional.empty() : Optional.of(k.getH().clone());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* D-081. The validator ADDRESS bound to a registry index at a height.
|
* D-081. The validator ADDRESS bound to a registry index at a height.
|
||||||
*
|
*
|
||||||
@ -4000,7 +4173,11 @@ public final class FalconSealSupport {
|
|||||||
* @return the bound address, or null when nothing binds that index at that height
|
* @return the bound address, or null when nothing binds that index at that height
|
||||||
*/
|
*/
|
||||||
public Address addressForIndexAtHistoric(final long blockNumber, final int validatorIndex) {
|
public Address addressForIndexAtHistoric(final long blockNumber, final int validatorIndex) {
|
||||||
return addressAt(blockNumber, validatorIndex, true);
|
// AERE AIP-22 stage 3b: from H_k the index names the v3 row directly (its id), like its key
|
||||||
|
if (AerePqIdentity.keysFromV3(blockNumber)) {
|
||||||
|
return AerePqIdentity.v3IdAt(blockNumber, validatorIndex).orElse(null);
|
||||||
|
}
|
||||||
|
return identityOf(addressAt(blockNumber, validatorIndex, true), blockNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4011,7 +4188,16 @@ public final class FalconSealSupport {
|
|||||||
* @return the bound address, or null
|
* @return the bound address, or null
|
||||||
*/
|
*/
|
||||||
public Address addressForIndexAtOwnHead(final long blockNumber, final int validatorIndex) {
|
public Address addressForIndexAtOwnHead(final long blockNumber, final int validatorIndex) {
|
||||||
return addressAt(blockNumber, validatorIndex, false);
|
// AERE AIP-22 stage 3b: from H_k the index names the v3 row directly (its id), like its key
|
||||||
|
if (AerePqIdentity.keysFromV3(blockNumber)) {
|
||||||
|
return AerePqIdentity.v3IdAt(blockNumber, validatorIndex).orElse(null);
|
||||||
|
}
|
||||||
|
return identityOf(addressAt(blockNumber, validatorIndex, false), blockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AERE AIP-22 stage 3: the registry's classical address for an index, as the identity of the height. */
|
||||||
|
private static Address identityOf(final Address registryAddress, final long blockNumber) {
|
||||||
|
return registryAddress == null ? null : AerePqIdentity.author(registryAddress, blockNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -43,10 +43,20 @@ public final class HybridSealProducer {
|
|||||||
private final PqSchemeSchedule schedule;
|
private final PqSchemeSchedule schedule;
|
||||||
private final int validatorIndex;
|
private final int validatorIndex;
|
||||||
private final Map<String, SealScheme.PrivateHandle> localKeys;
|
private final Map<String, SealScheme.PrivateHandle> localKeys;
|
||||||
|
// AERE AIP-22 stage 3b-ii: this node's NEXT keys per scheme (rotations), and its local index by height (from H_k the index of
|
||||||
|
// its Falcon seal at that height, since an extra seal must sit at the index of its Falcon seal)
|
||||||
|
private final Map<String, List<SealScheme.PrivateHandle>> nextKeys;
|
||||||
|
private final java.util.function.LongToIntFunction localIndexAt;
|
||||||
|
private final Map<String, Optional<SealScheme.PrivateHandle>> v3KeyByEpoch = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
// the origin of every anchor grid of the chain (AIP-22 stage 2: per-scheme intervals count from it)
|
||||||
|
private final long anchorBlock;
|
||||||
// one shout per missing scheme, not one per block: a permanent alarm is learned and ignored
|
// one shout per missing scheme, not one per block: a permanent alarm is learned and ignored
|
||||||
private final Set<String> alreadyShouted = new java.util.HashSet<>();
|
private final Set<String> alreadyShouted = new java.util.HashSet<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* A producer for schedules WITHOUT per-scheme intervals (every named scheme at every anchor),
|
||||||
|
* which is every schedule written before AIP-22 stage 2; the grid origin is irrelevant then.
|
||||||
|
*
|
||||||
* @param attachFromBlock first height at which extras may be emitted; {@link #NEVER} to disarm
|
* @param attachFromBlock first height at which extras may be emitted; {@link #NEVER} to disarm
|
||||||
* @param schedule which schemes are required at which height; null disarms as well
|
* @param schedule which schemes are required at which height; null disarms as well
|
||||||
* @param validatorIndex this node's index, written into every seal it produces
|
* @param validatorIndex this node's index, written into every seal it produces
|
||||||
@ -57,10 +67,94 @@ public final class HybridSealProducer {
|
|||||||
final PqSchemeSchedule schedule,
|
final PqSchemeSchedule schedule,
|
||||||
final int validatorIndex,
|
final int validatorIndex,
|
||||||
final Map<String, SealScheme.PrivateHandle> localKeys) {
|
final Map<String, SealScheme.PrivateHandle> localKeys) {
|
||||||
|
this(attachFromBlock, schedule, validatorIndex, localKeys, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param attachFromBlock first height at which extras may be emitted; {@link #NEVER} to disarm
|
||||||
|
* @param schedule which schemes are required at which height; null disarms as well
|
||||||
|
* @param validatorIndex this node's index, written into every seal it produces
|
||||||
|
* @param localKeys the private handles this node holds, per scheme id
|
||||||
|
* @param anchorBlock the chain's anchor activation height, origin of the per-scheme grids
|
||||||
|
*/
|
||||||
|
public HybridSealProducer(
|
||||||
|
final long attachFromBlock,
|
||||||
|
final PqSchemeSchedule schedule,
|
||||||
|
final int validatorIndex,
|
||||||
|
final Map<String, SealScheme.PrivateHandle> localKeys,
|
||||||
|
final long anchorBlock) {
|
||||||
this.attachFromBlock = schedule == null ? NEVER : attachFromBlock;
|
this.attachFromBlock = schedule == null ? NEVER : attachFromBlock;
|
||||||
this.schedule = schedule;
|
this.schedule = schedule;
|
||||||
this.validatorIndex = validatorIndex;
|
this.validatorIndex = validatorIndex;
|
||||||
this.localKeys = localKeys == null ? Map.of() : Map.copyOf(localKeys);
|
this.localKeys = localKeys == null ? Map.of() : Map.copyOf(localKeys);
|
||||||
|
this.anchorBlock = anchorBlock;
|
||||||
|
this.nextKeys = Map.of();
|
||||||
|
this.localIndexAt = h -> FalconSealSupport.instance().localIndexAt(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE AIP-22 stage 3b-ii: a producer that also holds NEXT keys and resolves its index by height.
|
||||||
|
*
|
||||||
|
* @param attachFromBlock first height at which extras may be emitted; {@link #NEVER} to disarm
|
||||||
|
* @param schedule which schemes are required at which height; null disarms as well
|
||||||
|
* @param validatorIndex this node's index below H_k
|
||||||
|
* @param localKeys the private handles this node holds, per scheme id
|
||||||
|
* @param anchorBlock the chain's anchor activation height, origin of the per-scheme grids
|
||||||
|
* @param nextKeys the next private handles per scheme id, for v3 epochs that rotate this node's keys
|
||||||
|
* @param localIndexAt this node's index at a height (from H_k: the index of its Falcon seal there)
|
||||||
|
*/
|
||||||
|
public HybridSealProducer(
|
||||||
|
final long attachFromBlock,
|
||||||
|
final PqSchemeSchedule schedule,
|
||||||
|
final int validatorIndex,
|
||||||
|
final Map<String, SealScheme.PrivateHandle> localKeys,
|
||||||
|
final long anchorBlock,
|
||||||
|
final Map<String, List<SealScheme.PrivateHandle>> nextKeys,
|
||||||
|
final java.util.function.LongToIntFunction localIndexAt) {
|
||||||
|
this.attachFromBlock = schedule == null ? NEVER : attachFromBlock;
|
||||||
|
this.schedule = schedule;
|
||||||
|
this.validatorIndex = validatorIndex;
|
||||||
|
this.localKeys = localKeys == null ? Map.of() : Map.copyOf(localKeys);
|
||||||
|
this.anchorBlock = anchorBlock;
|
||||||
|
this.nextKeys = nextKeys == null ? Map.of() : Map.copyOf(nextKeys);
|
||||||
|
this.localIndexAt = localIndexAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE AIP-22 stage 3b-ii: the key of a scheme this node signs with at a height from H_k, and the index it signs as: the local
|
||||||
|
* handle (base or next) whose probe signature verifies under the key of this node's row in the v3 epoch in force.
|
||||||
|
*
|
||||||
|
* @param scheme the scheme
|
||||||
|
* @param height the height
|
||||||
|
* @return the handle, or empty when this node holds no key of its row there (or has no row)
|
||||||
|
*/
|
||||||
|
public Optional<SealScheme.PrivateHandle> v3KeyAt(final SealScheme scheme, final long height) {
|
||||||
|
final int index = localIndexAt.applyAsInt(height);
|
||||||
|
if (index < 0) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
final long bind = AerePqIdentity.epochAt(height).map(PqRegistryV3.Registry::bindHeight).orElse(-1L);
|
||||||
|
return v3KeyByEpoch.computeIfAbsent(
|
||||||
|
bind + ":" + index + ":" + scheme.id(),
|
||||||
|
k -> {
|
||||||
|
final Optional<byte[]> rowKey = AerePqIdentity.v3KeyAt(height, index, scheme.id());
|
||||||
|
if (rowKey.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
final List<SealScheme.PrivateHandle> mine = new ArrayList<>();
|
||||||
|
if (localKeys.get(scheme.id()) != null) {
|
||||||
|
mine.add(localKeys.get(scheme.id()));
|
||||||
|
}
|
||||||
|
mine.addAll(nextKeys.getOrDefault(scheme.id(), List.of()));
|
||||||
|
final byte[] probe = ("AERE-HYBRID-KEY-PROBE-V3:" + bind + ":" + index).getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
for (final SealScheme.PrivateHandle h : mine) {
|
||||||
|
final Optional<byte[]> sig = scheme.sign(h, probe);
|
||||||
|
if (sig.isPresent() && scheme.verifyRaw(rowKey.get(), probe, sig.get())) {
|
||||||
|
return Optional.of(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A producer that never emits anything: the configuration of every node today. */
|
/** A producer that never emits anything: the configuration of every node today. */
|
||||||
@ -84,6 +178,11 @@ public final class HybridSealProducer {
|
|||||||
* <p>Never throws: a producer fault must never take down the ECDSA commit path. Every refusal
|
* <p>Never throws: a producer fault must never take down the ECDSA commit path. Every refusal
|
||||||
* is a logged reason plus an empty list, exactly the stance of the Falcon signer.
|
* is a logged reason plus an empty list, exactly the stance of the Falcon signer.
|
||||||
*
|
*
|
||||||
|
* <p>The block being committed is the PARENT of the anchor the seals serve (the caller only asks
|
||||||
|
* at anchor parents, D-329), so the schemes are those the anchor at {@code blockNumber + 1}
|
||||||
|
* requires: with per-scheme intervals (AIP-22 stage 2) a scheme on a coarser grid is not
|
||||||
|
* signed at the parent of an anchor that does not demand it.
|
||||||
|
*
|
||||||
* @param blockNumber the height of the block being committed
|
* @param blockNumber the height of the block being committed
|
||||||
* @param message the very bytes the Falcon seal of this commit signs
|
* @param message the very bytes the Falcon seal of this commit signs
|
||||||
* @return the seals, or empty when the gate is shut, a key is missing, or signing failed
|
* @return the seals, or empty when the gate is shut, a key is missing, or signing failed
|
||||||
@ -93,7 +192,7 @@ public final class HybridSealProducer {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final Set<String> required = schedule.schemesAt(blockNumber);
|
final Set<String> required = schedule.schemesRequiredForAnchor(blockNumber + 1L, anchorBlock);
|
||||||
final List<SchemeSeal> produced = new ArrayList<>();
|
final List<SchemeSeal> produced = new ArrayList<>();
|
||||||
for (final String schemeId : required) {
|
for (final String schemeId : required) {
|
||||||
if (SealSchemes.FALCON_512.id().equals(schemeId)) {
|
if (SealSchemes.FALCON_512.id().equals(schemeId)) {
|
||||||
@ -105,7 +204,11 @@ public final class HybridSealProducer {
|
|||||||
+ "' which this binary does not implement");
|
+ "' which this binary does not implement");
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
final SealScheme.PrivateHandle key = localKeys.get(schemeId);
|
// AERE AIP-22 stage 3b-ii: from H_k the key of this node's row in the v3 epoch in force, at its Falcon index there
|
||||||
|
final boolean v3 = AerePqIdentity.keysFromV3(blockNumber);
|
||||||
|
final SealScheme.PrivateHandle key =
|
||||||
|
v3 ? v3KeyAt(scheme.get(), blockNumber).orElse(null) : localKeys.get(schemeId);
|
||||||
|
final int index = v3 ? localIndexAt.applyAsInt(blockNumber) : validatorIndex;
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
shoutOnce(schemeId, "this node holds NO " + schemeId
|
shoutOnce(schemeId, "this node holds NO " + schemeId
|
||||||
+ " signing key, so it cannot produce the certificate the schedule requires from"
|
+ " signing key, so it cannot produce the certificate the schedule requires from"
|
||||||
@ -118,7 +221,7 @@ public final class HybridSealProducer {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
produced.add(
|
produced.add(
|
||||||
new SchemeSeal(scheme.get().wireId(), validatorIndex, Bytes.wrap(signature.get())));
|
new SchemeSeal(scheme.get().wireId(), index, Bytes.wrap(signature.get())));
|
||||||
}
|
}
|
||||||
// Canonical order, so two honest nodes signing the same block emit identical bytes and the
|
// Canonical order, so two honest nodes signing the same block emit identical bytes and the
|
||||||
// certificate cannot become a source of gratuitous divergence.
|
// certificate cannot become a source of gratuitous divergence.
|
||||||
|
|||||||
@ -164,6 +164,26 @@ public final class HybridSealSupport {
|
|||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"AERE-PQC-HYBRID-CONF-04: unparseable " + PROPERTY_SCHEDULE + ": " + e.getMessage());
|
"AERE-PQC-HYBRID-CONF-04: unparseable " + PROPERTY_SCHEDULE + ": " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
// AIP-22 stage 2: a per-scheme interval must be a multiple of the anchor interval this node
|
||||||
|
// runs, otherwise the scheme would be demanded at heights that are not anchors at all, and
|
||||||
|
// the two grids are only known together here. Refuse at startup, never run half-armed.
|
||||||
|
final PqAnchorConfig anchorConfig =
|
||||||
|
org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.config();
|
||||||
|
if (schedule.hasIntervals() && anchorConfig.anchorInterval().isPresent()) {
|
||||||
|
final int base = anchorConfig.anchorInterval().getAsInt();
|
||||||
|
for (final PqSchemeSchedule.Step step : schedule.steps()) {
|
||||||
|
for (final Map.Entry<String, Integer> e : step.intervals().entrySet()) {
|
||||||
|
if (e.getValue() % base != 0) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"AERE-PQC-HYBRID-CONF-06: " + PROPERTY_SCHEDULE + " gives scheme '" + e.getKey()
|
||||||
|
+ "' the interval " + e.getValue() + " at " + step.fromBlock()
|
||||||
|
+ ", which is not a multiple of this node's anchor interval " + base
|
||||||
|
+ " (" + PqAnchorConfig.PROPERTY_ANCHOR_INTERVAL + "); the scheme would be"
|
||||||
|
+ " demanded at heights that carry no certificate. Refusing to start.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
final HybridSignerRegistry registry;
|
final HybridSignerRegistry registry;
|
||||||
try {
|
try {
|
||||||
final Properties p = new Properties();
|
final Properties p = new Properties();
|
||||||
@ -253,6 +273,35 @@ public final class HybridSealSupport {
|
|||||||
index);
|
index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AERE AIP-22 stage 3b-ii: this node's NEXT keys per scheme (aere.pq.hybrid.key.<scheme>.next, comma list, same file
|
||||||
|
// format). They belong to later v3 epochs, so no registry probe here: the startup guard checks each against its v3 row.
|
||||||
|
final Map<String, java.util.List<SealScheme.PrivateHandle>> next = new HashMap<>();
|
||||||
|
for (final SealScheme scheme : SealSchemes.all()) {
|
||||||
|
if (scheme.id().equals(SealSchemes.FALCON_512.id())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String paths = reader.property(PROPERTY_KEY_PREFIX + scheme.id() + ".next");
|
||||||
|
if (paths == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (final String np : com.google.common.base.Splitter.on(',').omitEmptyStrings().trimResults().split(paths)) {
|
||||||
|
if (np.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final Properties kp = new Properties();
|
||||||
|
kp.load(new java.io.StringReader(new String(reader.file(np.trim()), StandardCharsets.UTF_8)));
|
||||||
|
final byte[] sk = Bytes.fromHexStringLenient(kp.getProperty("sk", "").trim()).toArray();
|
||||||
|
next.computeIfAbsent(scheme.id(), x -> new java.util.ArrayList<>())
|
||||||
|
.add(scheme.parsePrivateKey(sk).orElseThrow(() -> new IllegalStateException("bytes do not parse as a private key")));
|
||||||
|
LOG.info("AERE HIBRID: loaded a NEXT {} signing key from {} (AIP-22 stage 3b-ii)", scheme.id(), np.trim());
|
||||||
|
} catch (final IOException | RuntimeException e) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"AERE-PQC-KEY-NEXT-01: cannot load the next " + scheme.id() + " key from '" + np.trim() + "': " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (attachFrom != HybridSealProducer.NEVER && keys.isEmpty()) {
|
if (attachFrom != HybridSealProducer.NEVER && keys.isEmpty()) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"AERE-PQC-HYBRID-CONF-04: emission is armed from " + attachFrom + " but this node"
|
"AERE-PQC-HYBRID-CONF-04: emission is armed from " + attachFrom + " but this node"
|
||||||
@ -263,7 +312,14 @@ public final class HybridSealSupport {
|
|||||||
final HybridSealProducer producer =
|
final HybridSealProducer producer =
|
||||||
keys.isEmpty()
|
keys.isEmpty()
|
||||||
? HybridSealProducer.disarmed()
|
? HybridSealProducer.disarmed()
|
||||||
: new HybridSealProducer(attachFrom, schedule, boundIndex, keys);
|
: new HybridSealProducer(
|
||||||
|
attachFrom,
|
||||||
|
schedule,
|
||||||
|
boundIndex,
|
||||||
|
keys,
|
||||||
|
anchorConfig.anchorBlock(),
|
||||||
|
next,
|
||||||
|
h -> FalconSealSupport.instance().localIndexAt(h));
|
||||||
return new HybridSealSupport(schedule, registry, producer);
|
return new HybridSealSupport(schedule, registry, producer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,4 +348,36 @@ public final class HybridSealSupport {
|
|||||||
public HybridSealProducer producer() {
|
public HybridSealProducer producer() {
|
||||||
return producer;
|
return producer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE AIP-22 stage 3b-ii, THE STARTUP GUARD of the local hybrid keys: in every v3 epoch from H_k where this node's identity is
|
||||||
|
* registered, it must hold, for every non-Falcon scheme the schedule names at that epoch, the key of its row. Otherwise it would
|
||||||
|
* produce no hybrid seal from that epoch on while every configuration gate stays green; refuse to start instead.
|
||||||
|
*
|
||||||
|
* @param thisNodeAddress this node's own validator (ECDSA) address
|
||||||
|
*/
|
||||||
|
public void verifyLocalKeysCoverV3EpochsOrAbort(final org.hyperledger.besu.datatypes.Address thisNodeAddress) {
|
||||||
|
final Optional<AerePqIdentity.Config> c = AerePqIdentity.installed();
|
||||||
|
if (schedule == null || c.isEmpty() || c.get().keysFrom() == AerePqIdentity.NEVER || thisNodeAddress == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (final PqRegistryV3.Registry e : c.get().epochs().tailMap(c.get().keysFrom(), true).values()) {
|
||||||
|
final org.hyperledger.besu.datatypes.Address me = AerePqIdentity.author(thisNodeAddress, e.bindHeight());
|
||||||
|
if (e.rowOf(me.getBytes().toArrayUnsafe()).isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (final String schemeId : schedule.schemesAt(e.bindHeight())) {
|
||||||
|
if (schemeId.equals(SealSchemes.FALCON_512.id())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Optional<SealScheme> scheme = SealSchemes.byId(schemeId);
|
||||||
|
if (scheme.isEmpty() || producer.v3KeyAt(scheme.get(), e.bindHeight()).isEmpty()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"AERE-PQC-KEY-NEXT-02: REFUSING TO START. This node (id " + me + ") is registered in the v3 epoch bound at "
|
||||||
|
+ e.bindHeight() + " but holds no " + schemeId + " key of its row there (aere.pq.hybrid.key." + schemeId
|
||||||
|
+ "[.next]); from that height it would produce no hybrid seal.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,963 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3: the validator registry v3 (post-quantum identity).
|
||||||
|
*
|
||||||
|
* WHAT IT IS. One registry epoch as a JSON manifest: rows keyed "0".."count-1" plus formatVersion 3,
|
||||||
|
* chainId, bindHeight and count at top level. Each row names a validator by a 20-byte `id` derived
|
||||||
|
* from its Falcon-512 key (AIP-22 section 3.1, the AIP-20 sender derivation with alg id 0x20):
|
||||||
|
*
|
||||||
|
* id = keccak256(0x50 || 0x20 || falcon512_public_key_897)[12:32]
|
||||||
|
*
|
||||||
|
* and binds to it one public key per seal scheme, each with a proof of possession. The id is taken
|
||||||
|
* at the first registration and KEPT through key rotations: a later epoch may change a row's keys
|
||||||
|
* only with a rotation signature by EVERY key the previous epoch bound to that id. The `ecdsa`
|
||||||
|
* field ({addr, claim}) is the verifiable link from a classical validator address to the id, and is
|
||||||
|
* required in the epoch that performs the switch at H_id. `transport` names the RLPx node key a
|
||||||
|
* validator's client uses, for the peer-forwarding predicate (AIP-22 section 3.4).
|
||||||
|
*
|
||||||
|
* THE CANONICAL FORMS, pinned here and in the conformance vectors (AIP-22 section 3.2 names them;
|
||||||
|
* this class is where their bytes are fixed, and the Nethermind port and the public JS tool must
|
||||||
|
* produce the same bytes from the same vector files):
|
||||||
|
*
|
||||||
|
* row pre-image = "AERE-PQ-ROW-3" || u8(3) || u64be(chainId) || u64be(bindHeight) || u32be(count)
|
||||||
|
* || u32be(index) || id(20) || u8(nKeys)
|
||||||
|
* || for each key in ascending wire id: u8(wireId) || u32be(len pk) || pk
|
||||||
|
* || u32be(len pop) || pop
|
||||||
|
* || u8(hasTransport) [|| transport(20)]
|
||||||
|
* row hash = keccak256(row pre-image) (rotation and ecdsa are NOT in it)
|
||||||
|
* Merkle root = binary tree over the row hashes in index order; node = keccak256(0x01 || l || r);
|
||||||
|
* an odd node at any level is paired with itself; one row: the root is its hash
|
||||||
|
* registry hash = keccak256("AERE-PQ-REGISTRY-3" || u64be(chainId) || u64be(bindHeight)
|
||||||
|
* || u32be(count) || root)
|
||||||
|
* pop message = keccak256("AERE-PQ-POP-3" || u8(3) || u64be(chainId) || u64be(bindHeight)
|
||||||
|
* || u32be(count) || u32be(index) || id(20) || u8(wireId) || u32be(len pk) || pk)
|
||||||
|
* rotate message = keccak256("AERE-PQ-ROTATE-3" || new row hash)
|
||||||
|
* claim digest = keccak256(0x19 || 0x00 || context || new row hash), context = the low 20 bytes
|
||||||
|
* of keccak256("AERE-PQ-CLAIM-1" || u64be(chainId)), the EIP-191 envelope of v2
|
||||||
|
*
|
||||||
|
* The public key in the pre-images is the STANDARD encoding: Falcon-512 897 bytes (header 0x09 +
|
||||||
|
* the 896-byte h polynomial the v1/v2 registries store), SLH-DSA-SHA2-128s 32 bytes. Every field
|
||||||
|
* of the pop message earns its place for the reason given in PqRegistryBinding (domain tag against
|
||||||
|
* seal replay, key in the message against Falcon's missing exclusive ownership, chainId, index,
|
||||||
|
* bindHeight and count against lifting and truncation); the wire id keeps one scheme's proof from
|
||||||
|
* standing in for another's. The Merkle node tag 0x01 keeps an inner node from ever being read as
|
||||||
|
* a row hash, and the count bound into the registry hash keeps a duplicated odd leaf from making a
|
||||||
|
* tree of count+1 rows hash like one of count.
|
||||||
|
*
|
||||||
|
* LOADING IS STRICT, as for v1/v2: an ambiguous or incomplete file REFUSES with a named code, never
|
||||||
|
* hashes. Refusal codes AERE-PQC-REG3-01..16, listed at {@link Code}.
|
||||||
|
*
|
||||||
|
* WHAT IT DOES NOT DO (stated): it does not wire the ids into QBFT (the set translation at H_id,
|
||||||
|
* message authorship, coinbase, votes and peer forwarding are later parts of stage 3), and it
|
||||||
|
* does not pin the registry hash on chain (the anchor-contract slot or genesis pqRegistryHash
|
||||||
|
* does that, as today; {@link #loadPinned} compares against the pinned value).
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft;
|
||||||
|
|
||||||
|
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 java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.bouncycastle.crypto.digests.KeccakDigest;
|
||||||
|
|
||||||
|
/** The v3 validator registry: canonical forms, strict loader, Merkle membership. */
|
||||||
|
public final class PqRegistryV3 {
|
||||||
|
|
||||||
|
/** Format version of this registry. */
|
||||||
|
public static final int FORMAT_VERSION = 3;
|
||||||
|
|
||||||
|
/** Domain tag of the row pre-image. */
|
||||||
|
public static final String DOMAIN_ROW = "AERE-PQ-ROW-3";
|
||||||
|
|
||||||
|
/** Domain tag of the registry hash. */
|
||||||
|
public static final String DOMAIN_REGISTRY = "AERE-PQ-REGISTRY-3";
|
||||||
|
|
||||||
|
/** Domain tag of the proof-of-possession message. */
|
||||||
|
public static final String DOMAIN_POP = "AERE-PQ-POP-3";
|
||||||
|
|
||||||
|
/** Domain tag of the rotation message. */
|
||||||
|
public static final String DOMAIN_ROTATE = "AERE-PQ-ROTATE-3";
|
||||||
|
|
||||||
|
/** Tag byte of an inner Merkle node. */
|
||||||
|
public static final byte MERKLE_NODE_TAG = 0x01;
|
||||||
|
|
||||||
|
/** First byte of the id pre-image: the AIP-20 transaction type. */
|
||||||
|
public static final byte ID_TX_TYPE = 0x50;
|
||||||
|
|
||||||
|
/** Second byte of the id pre-image: the algorithm id AIP-20 reserves for Falcon-512. */
|
||||||
|
public static final byte ID_ALG_FALCON_512 = 0x20;
|
||||||
|
|
||||||
|
/** Header byte of the standard Falcon-512 public key encoding. */
|
||||||
|
public static final byte FALCON_512_PK_HEADER = 0x09;
|
||||||
|
|
||||||
|
/** Standard Falcon-512 public key length (header + 896-byte h). */
|
||||||
|
public static final int FALCON_512_PK_LENGTH = 897;
|
||||||
|
|
||||||
|
private static final int ECDSA_SIGNATURE_LENGTH = 65;
|
||||||
|
|
||||||
|
/** The named refusals of the strict loader. */
|
||||||
|
public enum Code {
|
||||||
|
/** formatVersion missing or not 3. */
|
||||||
|
REG3_01_FORMAT("AERE-PQC-REG3-01"),
|
||||||
|
/** chainId missing or not the node's chain. */
|
||||||
|
REG3_02_CHAIN("AERE-PQC-REG3-02"),
|
||||||
|
/** count missing, rows not exactly "0".."count-1", or an unknown top-level or row field. */
|
||||||
|
REG3_03_SHAPE("AERE-PQC-REG3-03"),
|
||||||
|
/** id missing, not hex, or not 20 bytes. */
|
||||||
|
REG3_04_ID("AERE-PQC-REG3-04"),
|
||||||
|
/** a key names a scheme this build does not know. */
|
||||||
|
REG3_05_UNKNOWN_SCHEME("AERE-PQC-REG3-05"),
|
||||||
|
/** a scheme the schedule requires has no key on a row. */
|
||||||
|
REG3_06_REQUIRED_SCHEME("AERE-PQC-REG3-06"),
|
||||||
|
/** a public key is not in its scheme's standard encoding. */
|
||||||
|
REG3_07_KEY_ENCODING("AERE-PQC-REG3-07"),
|
||||||
|
/** a proof of possession is missing or does not verify. */
|
||||||
|
REG3_08_POP("AERE-PQC-REG3-08"),
|
||||||
|
/** a NEW id is not the derivation of the row's Falcon-512 key. */
|
||||||
|
REG3_09_ID_DERIVATION("AERE-PQC-REG3-09"),
|
||||||
|
/** two rows carry the same id. */
|
||||||
|
REG3_10_DUPLICATE_ID("AERE-PQC-REG3-10"),
|
||||||
|
/** two rows carry the same public key. */
|
||||||
|
REG3_11_DUPLICATE_KEY("AERE-PQC-REG3-11"),
|
||||||
|
/** a changed row lacks a verifying rotation signature by every previous key of its id. */
|
||||||
|
REG3_12_ROTATION_MISSING("AERE-PQC-REG3-12"),
|
||||||
|
/** a rotation is present where none belongs (unchanged row, or a new id). */
|
||||||
|
REG3_13_ROTATION_UNEXPECTED("AERE-PQC-REG3-13"),
|
||||||
|
/** the ecdsa link is required and missing, does not recover to its address, or repeats one. */
|
||||||
|
REG3_14_ECDSA("AERE-PQC-REG3-14"),
|
||||||
|
/** transport is malformed. */
|
||||||
|
REG3_15_TRANSPORT("AERE-PQC-REG3-15"),
|
||||||
|
/** the computed registry hash is not the pinned (or the file's own declared) hash. */
|
||||||
|
REG3_16_HASH("AERE-PQC-REG3-16");
|
||||||
|
|
||||||
|
private final String text;
|
||||||
|
|
||||||
|
Code(final String text) {
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The refusal code as printed.
|
||||||
|
*
|
||||||
|
* @return e.g. AERE-PQC-REG3-08
|
||||||
|
*/
|
||||||
|
public String text() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A refusal of the strict loader, carrying its named code. */
|
||||||
|
public static final class RegistryV3Exception extends RuntimeException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** The named refusal. */
|
||||||
|
private final transient Code code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A refusal.
|
||||||
|
*
|
||||||
|
* @param code the named code
|
||||||
|
* @param message what was wrong, in words
|
||||||
|
*/
|
||||||
|
public RegistryV3Exception(final Code code, final String message) {
|
||||||
|
super(code.text() + ": " + message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The named code.
|
||||||
|
*
|
||||||
|
* @return the code
|
||||||
|
*/
|
||||||
|
public Code code() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One key of a row.
|
||||||
|
*
|
||||||
|
* @param scheme the seal scheme
|
||||||
|
* @param publicKey the standard encoding (Falcon-512 897 bytes, SLH-DSA-SHA2-128s 32 bytes)
|
||||||
|
* @param pop the proof of possession by that key
|
||||||
|
*/
|
||||||
|
public record Key(SealScheme scheme, byte[] publicKey, byte[] pop) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row.
|
||||||
|
*
|
||||||
|
* @param index the row index
|
||||||
|
* @param id the 20-byte validator id
|
||||||
|
* @param keys the keys by wire id, ascending
|
||||||
|
* @param transport the RLPx node-key address, or null
|
||||||
|
* @param rotation scheme id to rotation signature (by the previous epoch's key of that scheme)
|
||||||
|
* @param ecdsaAddress the classical validator address, or null
|
||||||
|
* @param ecdsaClaim the EIP-191 claim by that address over the row hash, or null
|
||||||
|
*/
|
||||||
|
public record Row(
|
||||||
|
int index,
|
||||||
|
byte[] id,
|
||||||
|
TreeMap<Integer, Key> keys,
|
||||||
|
byte[] transport,
|
||||||
|
Map<String, byte[]> rotation,
|
||||||
|
byte[] ecdsaAddress,
|
||||||
|
byte[] ecdsaClaim) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key of a scheme on this row.
|
||||||
|
*
|
||||||
|
* @param schemeId e.g. "falcon-512"
|
||||||
|
* @return the key, if the row carries one
|
||||||
|
*/
|
||||||
|
public Optional<Key> key(final String schemeId) {
|
||||||
|
for (final Key k : keys.values()) {
|
||||||
|
if (k.scheme().id().equals(schemeId)) {
|
||||||
|
return Optional.of(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A loaded, verified epoch.
|
||||||
|
*
|
||||||
|
* @param chainId the chain
|
||||||
|
* @param bindHeight the activation height of this epoch
|
||||||
|
* @param count the number of rows
|
||||||
|
* @param rows the rows in index order
|
||||||
|
* @param rowHashes the row hashes in index order
|
||||||
|
* @param root the Merkle root over the row hashes
|
||||||
|
* @param hash the registry hash (what the chain pins)
|
||||||
|
*/
|
||||||
|
public record Registry(
|
||||||
|
long chainId,
|
||||||
|
long bindHeight,
|
||||||
|
int count,
|
||||||
|
List<Row> rows,
|
||||||
|
List<Bytes32> rowHashes,
|
||||||
|
Bytes32 root,
|
||||||
|
Bytes32 hash) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The row of an id.
|
||||||
|
*
|
||||||
|
* @param id the 20-byte id
|
||||||
|
* @return the row, if the id is registered in this epoch
|
||||||
|
*/
|
||||||
|
public Optional<Row> rowOf(final byte[] id) {
|
||||||
|
for (final Row r : rows) {
|
||||||
|
if (Arrays.equals(r.id(), id)) {
|
||||||
|
return Optional.of(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The row whose ecdsa link names this classical address.
|
||||||
|
*
|
||||||
|
* @param address the 20-byte address
|
||||||
|
* @return the row, if one links it
|
||||||
|
*/
|
||||||
|
public Optional<Row> rowOfEcdsa(final byte[] address) {
|
||||||
|
for (final Row r : rows) {
|
||||||
|
if (r.ecdsaAddress() != null && Arrays.equals(r.ecdsaAddress(), address)) {
|
||||||
|
return Optional.of(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PqRegistryV3() {}
|
||||||
|
|
||||||
|
// =============================================================================================
|
||||||
|
// Canonical forms
|
||||||
|
// =============================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The validator id of a Falcon-512 key (AIP-22 section 3.1).
|
||||||
|
*
|
||||||
|
* @param falconStandard897 the 897-byte standard encoding
|
||||||
|
* @return the 20-byte id
|
||||||
|
*/
|
||||||
|
public static byte[] idOf(final byte[] falconStandard897) {
|
||||||
|
final byte[] in = new byte[2 + falconStandard897.length];
|
||||||
|
in[0] = ID_TX_TYPE;
|
||||||
|
in[1] = ID_ALG_FALCON_512;
|
||||||
|
System.arraycopy(falconStandard897, 0, in, 2, falconStandard897.length);
|
||||||
|
return Arrays.copyOfRange(keccak(in), 12, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message a row key signs to prove possession.
|
||||||
|
*
|
||||||
|
* @param chainId the chain
|
||||||
|
* @param bindHeight the epoch's activation height
|
||||||
|
* @param count the number of rows
|
||||||
|
* @param index the row index
|
||||||
|
* @param id the row id
|
||||||
|
* @param wireId the scheme wire id
|
||||||
|
* @param publicKey the standard encoding of the key
|
||||||
|
* @return the 32-byte message
|
||||||
|
*/
|
||||||
|
public static Bytes32 popMessage(
|
||||||
|
final long chainId,
|
||||||
|
final long bindHeight,
|
||||||
|
final int count,
|
||||||
|
final int index,
|
||||||
|
final byte[] id,
|
||||||
|
final byte wireId,
|
||||||
|
final byte[] publicKey) {
|
||||||
|
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
writeAll(out, DOMAIN_POP.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
out.write(FORMAT_VERSION);
|
||||||
|
writeAll(out, uint64be(chainId));
|
||||||
|
writeAll(out, uint64be(bindHeight));
|
||||||
|
writeAll(out, uint32be(count));
|
||||||
|
writeAll(out, uint32be(index));
|
||||||
|
writeAll(out, id);
|
||||||
|
out.write(wireId);
|
||||||
|
writeAll(out, uint32be(publicKey.length));
|
||||||
|
writeAll(out, publicKey);
|
||||||
|
return Bytes32.wrap(keccak(out.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The canonical pre-image of a row (without rotation and ecdsa).
|
||||||
|
*
|
||||||
|
* @param chainId the chain
|
||||||
|
* @param bindHeight the epoch's activation height
|
||||||
|
* @param count the number of rows
|
||||||
|
* @param row the row
|
||||||
|
* @return the pre-image
|
||||||
|
*/
|
||||||
|
public static byte[] rowPreimage(
|
||||||
|
final long chainId, final long bindHeight, final int count, final Row row) {
|
||||||
|
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
writeAll(out, DOMAIN_ROW.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
out.write(FORMAT_VERSION);
|
||||||
|
writeAll(out, uint64be(chainId));
|
||||||
|
writeAll(out, uint64be(bindHeight));
|
||||||
|
writeAll(out, uint32be(count));
|
||||||
|
writeAll(out, uint32be(row.index()));
|
||||||
|
writeAll(out, row.id());
|
||||||
|
out.write(row.keys().size());
|
||||||
|
for (final Map.Entry<Integer, Key> e : row.keys().entrySet()) {
|
||||||
|
out.write(e.getKey());
|
||||||
|
writeAll(out, uint32be(e.getValue().publicKey().length));
|
||||||
|
writeAll(out, e.getValue().publicKey());
|
||||||
|
writeAll(out, uint32be(e.getValue().pop().length));
|
||||||
|
writeAll(out, e.getValue().pop());
|
||||||
|
}
|
||||||
|
if (row.transport() == null) {
|
||||||
|
out.write(0);
|
||||||
|
} else {
|
||||||
|
out.write(1);
|
||||||
|
writeAll(out, row.transport());
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The row hash.
|
||||||
|
*
|
||||||
|
* @param chainId the chain
|
||||||
|
* @param bindHeight the epoch's activation height
|
||||||
|
* @param count the number of rows
|
||||||
|
* @param row the row
|
||||||
|
* @return keccak256 of the row pre-image
|
||||||
|
*/
|
||||||
|
public static Bytes32 rowHash(
|
||||||
|
final long chainId, final long bindHeight, final int count, final Row row) {
|
||||||
|
return Bytes32.wrap(keccak(rowPreimage(chainId, bindHeight, count, row)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Merkle root over row hashes in index order.
|
||||||
|
*
|
||||||
|
* @param leaves the row hashes
|
||||||
|
* @return the root
|
||||||
|
*/
|
||||||
|
public static Bytes32 merkleRoot(final List<Bytes32> leaves) {
|
||||||
|
if (leaves.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("a registry has at least one row");
|
||||||
|
}
|
||||||
|
List<Bytes32> level = leaves;
|
||||||
|
while (level.size() > 1) {
|
||||||
|
final List<Bytes32> next = new ArrayList<>();
|
||||||
|
for (int i = 0; i < level.size(); i += 2) {
|
||||||
|
final Bytes32 l = level.get(i);
|
||||||
|
final Bytes32 r = i + 1 < level.size() ? level.get(i + 1) : l;
|
||||||
|
next.add(node(l, r));
|
||||||
|
}
|
||||||
|
level = next;
|
||||||
|
}
|
||||||
|
return level.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sibling path of one leaf, bottom-up.
|
||||||
|
*
|
||||||
|
* @param leaves the row hashes
|
||||||
|
* @param index the leaf index
|
||||||
|
* @return the siblings from the leaf level up
|
||||||
|
*/
|
||||||
|
public static List<Bytes32> merkleProof(final List<Bytes32> leaves, final int index) {
|
||||||
|
if (index < 0 || index >= leaves.size()) {
|
||||||
|
throw new IllegalArgumentException("index " + index + " is not a row of " + leaves.size());
|
||||||
|
}
|
||||||
|
final List<Bytes32> proof = new ArrayList<>();
|
||||||
|
List<Bytes32> level = leaves;
|
||||||
|
int i = index;
|
||||||
|
while (level.size() > 1) {
|
||||||
|
final int sib = (i % 2 == 0) ? i + 1 : i - 1;
|
||||||
|
proof.add(sib < level.size() ? level.get(sib) : level.get(i));
|
||||||
|
final List<Bytes32> next = new ArrayList<>();
|
||||||
|
for (int j = 0; j < level.size(); j += 2) {
|
||||||
|
final Bytes32 l = level.get(j);
|
||||||
|
final Bytes32 r = j + 1 < level.size() ? level.get(j + 1) : l;
|
||||||
|
next.add(node(l, r));
|
||||||
|
}
|
||||||
|
level = next;
|
||||||
|
i /= 2;
|
||||||
|
}
|
||||||
|
return proof;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a membership proof. The index MUST be below count: with the odd leaf paired with
|
||||||
|
* itself, a path for index == count would otherwise verify against a count-row tree.
|
||||||
|
*
|
||||||
|
* @param leaf the row hash
|
||||||
|
* @param index the claimed index
|
||||||
|
* @param count the number of rows
|
||||||
|
* @param proof the siblings, bottom-up
|
||||||
|
* @param root the root
|
||||||
|
* @return whether the leaf is row {@code index} of the tree with that root
|
||||||
|
*/
|
||||||
|
public static boolean verifyMerkleProof(
|
||||||
|
final Bytes32 leaf,
|
||||||
|
final int index,
|
||||||
|
final int count,
|
||||||
|
final List<Bytes32> proof,
|
||||||
|
final Bytes32 root) {
|
||||||
|
if (index < 0 || index >= count || count < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int depth = 0;
|
||||||
|
for (int n = count; n > 1; n = (n + 1) / 2) {
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
if (proof.size() != depth) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Bytes32 acc = leaf;
|
||||||
|
int i = index;
|
||||||
|
for (final Bytes32 sib : proof) {
|
||||||
|
acc = (i % 2 == 0) ? node(acc, sib) : node(sib, acc);
|
||||||
|
i /= 2;
|
||||||
|
}
|
||||||
|
return acc.equals(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The registry hash the chain pins.
|
||||||
|
*
|
||||||
|
* @param chainId the chain
|
||||||
|
* @param bindHeight the epoch's activation height
|
||||||
|
* @param count the number of rows
|
||||||
|
* @param root the Merkle root
|
||||||
|
* @return the hash
|
||||||
|
*/
|
||||||
|
public static Bytes32 registryHash(
|
||||||
|
final long chainId, final long bindHeight, final int count, final Bytes32 root) {
|
||||||
|
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
writeAll(out, DOMAIN_REGISTRY.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
writeAll(out, uint64be(chainId));
|
||||||
|
writeAll(out, uint64be(bindHeight));
|
||||||
|
writeAll(out, uint32be(count));
|
||||||
|
writeAll(out, root.toArrayUnsafe());
|
||||||
|
return Bytes32.wrap(keccak(out.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message every previous key of an id signs to authorise a changed row.
|
||||||
|
*
|
||||||
|
* @param newRowHash the hash of the new row
|
||||||
|
* @return the 32-byte message
|
||||||
|
*/
|
||||||
|
public static Bytes32 rotateMessage(final Bytes32 newRowHash) {
|
||||||
|
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
writeAll(out, DOMAIN_ROTATE.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
writeAll(out, newRowHash.toArrayUnsafe());
|
||||||
|
return Bytes32.wrap(keccak(out.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The digest the classical validator key signs to link its address to the row.
|
||||||
|
*
|
||||||
|
* @param chainId the chain
|
||||||
|
* @param rowHash the row hash
|
||||||
|
* @return the EIP-191 version-0 digest
|
||||||
|
*/
|
||||||
|
public static Bytes32 claimDigest(final long chainId, final Bytes32 rowHash) {
|
||||||
|
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(0x19);
|
||||||
|
out.write(0x00);
|
||||||
|
writeAll(out, PqRegistryBinding.claimContext(chainId));
|
||||||
|
writeAll(out, rowHash.toArrayUnsafe());
|
||||||
|
return Bytes32.wrap(keccak(out.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================================
|
||||||
|
// The strict loader
|
||||||
|
// =============================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and verifies an epoch and compares its hash with the pinned one.
|
||||||
|
*
|
||||||
|
* @param json the manifest
|
||||||
|
* @param chainId the node's chain
|
||||||
|
* @param requiredSchemes the scheme ids every row must carry
|
||||||
|
* @param previous the previous v3 epoch (for rotations), or null for the first
|
||||||
|
* @param ecdsaRequired whether every row must carry the ecdsa link (the switch epoch)
|
||||||
|
* @param pinnedHash the hash the chain pins
|
||||||
|
* @return the verified epoch
|
||||||
|
*/
|
||||||
|
public static Registry loadPinned(
|
||||||
|
final JsonNode json,
|
||||||
|
final long chainId,
|
||||||
|
final List<String> requiredSchemes,
|
||||||
|
final Registry previous,
|
||||||
|
final boolean ecdsaRequired,
|
||||||
|
final Bytes32 pinnedHash) {
|
||||||
|
final Registry r = load(json, chainId, requiredSchemes, previous, ecdsaRequired);
|
||||||
|
if (!r.hash().equals(pinnedHash)) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_16_HASH,
|
||||||
|
"the registry hashes to " + r.hash() + ", the chain pins " + pinnedHash + "; refusing");
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and verifies an epoch (every proof, every uniqueness rule, every rotation).
|
||||||
|
*
|
||||||
|
* @param json the manifest
|
||||||
|
* @param chainId the node's chain
|
||||||
|
* @param requiredSchemes the scheme ids every row must carry
|
||||||
|
* @param previous the previous v3 epoch (for rotations), or null for the first
|
||||||
|
* @param ecdsaRequired whether every row must carry the ecdsa link
|
||||||
|
* @return the verified epoch
|
||||||
|
*/
|
||||||
|
public static Registry load(
|
||||||
|
final JsonNode json,
|
||||||
|
final long chainId,
|
||||||
|
final List<String> requiredSchemes,
|
||||||
|
final Registry previous,
|
||||||
|
final boolean ecdsaRequired) {
|
||||||
|
if (json == null || !json.isObject()) {
|
||||||
|
throw new RegistryV3Exception(Code.REG3_03_SHAPE, "the manifest is not a JSON object");
|
||||||
|
}
|
||||||
|
if (!json.has("formatVersion") || json.get("formatVersion").asInt(-1) != FORMAT_VERSION
|
||||||
|
|| !json.get("formatVersion").isIntegralNumber()) {
|
||||||
|
throw new RegistryV3Exception(Code.REG3_01_FORMAT, "formatVersion must be the number 3");
|
||||||
|
}
|
||||||
|
if (!json.has("chainId") || !json.get("chainId").isIntegralNumber()
|
||||||
|
|| json.get("chainId").asLong() != chainId) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_02_CHAIN, "chainId must be the number " + chainId + " (this node's chain)");
|
||||||
|
}
|
||||||
|
if (!json.has("bindHeight") || !json.get("bindHeight").isIntegralNumber()
|
||||||
|
|| json.get("bindHeight").asLong() < 0) {
|
||||||
|
throw new RegistryV3Exception(Code.REG3_03_SHAPE, "bindHeight must be a number >= 0");
|
||||||
|
}
|
||||||
|
if (!json.has("count") || !json.get("count").isIntegralNumber()
|
||||||
|
|| json.get("count").asInt() < 1) {
|
||||||
|
throw new RegistryV3Exception(Code.REG3_03_SHAPE, "count must be a number >= 1");
|
||||||
|
}
|
||||||
|
final long bindHeight = json.get("bindHeight").asLong();
|
||||||
|
final int count = json.get("count").asInt();
|
||||||
|
final Set<String> allowedTop = new HashSet<>();
|
||||||
|
Collections.addAll(allowedTop, "formatVersion", "chainId", "bindHeight", "count", "registryHash");
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
allowedTop.add(Integer.toString(i));
|
||||||
|
}
|
||||||
|
final Iterator<String> names = json.fieldNames();
|
||||||
|
while (names.hasNext()) {
|
||||||
|
final String n = names.next();
|
||||||
|
if (!allowedTop.contains(n)) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_03_SHAPE,
|
||||||
|
"unexpected top-level field '" + n + "' (rows are exactly \"0\"..\"" + (count - 1)
|
||||||
|
+ "\")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final List<Row> rows = new ArrayList<>();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
final JsonNode e = json.get(Integer.toString(i));
|
||||||
|
if (e == null || !e.isObject()) {
|
||||||
|
throw new RegistryV3Exception(Code.REG3_03_SHAPE, "row " + i + " is missing");
|
||||||
|
}
|
||||||
|
rows.add(parseRow(i, e));
|
||||||
|
}
|
||||||
|
// required schemes, key encodings, possession proofs
|
||||||
|
for (final Row row : rows) {
|
||||||
|
for (final String s : requiredSchemes) {
|
||||||
|
if (row.key(s).isEmpty()) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_06_REQUIRED_SCHEME, "row " + row.index() + " has no " + s + " key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row.key(SealSchemes.FALCON_512.id()).isEmpty()) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_06_REQUIRED_SCHEME,
|
||||||
|
"row " + row.index() + " has no falcon-512 key; the id is derived from it");
|
||||||
|
}
|
||||||
|
for (final Key k : row.keys().values()) {
|
||||||
|
final byte[] registryForm = registryForm(row.index(), k);
|
||||||
|
final Bytes32 msg =
|
||||||
|
popMessage(
|
||||||
|
chainId, bindHeight, count, row.index(), row.id(), k.scheme().wireId(),
|
||||||
|
k.publicKey());
|
||||||
|
if (k.pop() == null || k.pop().length == 0
|
||||||
|
|| !k.scheme().verifyRaw(registryForm, msg.toArrayUnsafe(), k.pop())) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_08_POP,
|
||||||
|
"row " + row.index() + " " + k.scheme().id()
|
||||||
|
+ ": the proof of possession does not verify for chainId=" + chainId
|
||||||
|
+ " bindHeight=" + bindHeight + " count=" + count + " index=" + row.index());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// uniqueness of ids and of every public key
|
||||||
|
final Set<Bytes> ids = new HashSet<>();
|
||||||
|
final Set<Bytes> keys = new HashSet<>();
|
||||||
|
for (final Row row : rows) {
|
||||||
|
if (!ids.add(Bytes.wrap(row.id()))) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_10_DUPLICATE_ID, "id 0x" + hex(row.id()) + " appears on two rows");
|
||||||
|
}
|
||||||
|
for (final Key k : row.keys().values()) {
|
||||||
|
if (!keys.add(Bytes.wrap(k.publicKey()))) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_11_DUPLICATE_KEY,
|
||||||
|
"row " + row.index() + " " + k.scheme().id() + ": the key appears on another row");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// row hashes, then ids and rotations against the previous epoch
|
||||||
|
final List<Bytes32> hashes = new ArrayList<>();
|
||||||
|
for (final Row row : rows) {
|
||||||
|
hashes.add(rowHash(chainId, bindHeight, count, row));
|
||||||
|
}
|
||||||
|
for (final Row row : rows) {
|
||||||
|
final Optional<Row> prev = previous == null ? Optional.empty() : previous.rowOf(row.id());
|
||||||
|
if (prev.isEmpty()) {
|
||||||
|
final byte[] derived = idOf(row.key(SealSchemes.FALCON_512.id()).orElseThrow().publicKey());
|
||||||
|
if (!Arrays.equals(derived, row.id())) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_09_ID_DERIVATION,
|
||||||
|
"row " + row.index() + ": a new id must be keccak256(0x50||0x20||falcon)[12:32] = 0x"
|
||||||
|
+ hex(derived) + ", the row says 0x" + hex(row.id()));
|
||||||
|
}
|
||||||
|
if (!row.rotation().isEmpty()) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_13_ROTATION_UNEXPECTED,
|
||||||
|
"row " + row.index() + " is a new id and carries a rotation");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final boolean changed = !sameKeys(prev.get(), row);
|
||||||
|
if (!changed) {
|
||||||
|
if (!row.rotation().isEmpty()) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_13_ROTATION_UNEXPECTED,
|
||||||
|
"row " + row.index() + " did not change its keys and carries a rotation");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Bytes32 rot = rotateMessage(hashes.get(row.index()));
|
||||||
|
for (final Key old : prev.get().keys().values()) {
|
||||||
|
final byte[] sig = row.rotation().get(old.scheme().id());
|
||||||
|
if (sig == null
|
||||||
|
|| !old.scheme()
|
||||||
|
.verifyRaw(registryForm(prev.get().index(), old), rot.toArrayUnsafe(), sig)) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_12_ROTATION_MISSING,
|
||||||
|
"row " + row.index() + " changed its keys; the previous " + old.scheme().id()
|
||||||
|
+ " key of id 0x" + hex(row.id()) + " did not sign the new row (every previous"
|
||||||
|
+ " key must)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row.rotation().size() != prev.get().keys().size()) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_13_ROTATION_UNEXPECTED,
|
||||||
|
"row " + row.index() + " carries a rotation signature for a scheme the previous row"
|
||||||
|
+ " did not bind");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the ecdsa link
|
||||||
|
final Set<Bytes> ecdsa = new HashSet<>();
|
||||||
|
for (final Row row : rows) {
|
||||||
|
if (row.ecdsaAddress() == null) {
|
||||||
|
if (ecdsaRequired) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_14_ECDSA,
|
||||||
|
"row " + row.index() + " has no ecdsa link; this epoch performs the switch and"
|
||||||
|
+ " every row must carry one");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Address rec = recover(claimDigest(chainId, hashes.get(row.index())), row.ecdsaClaim());
|
||||||
|
if (rec == null || !Arrays.equals(rec.getBytes().toArrayUnsafe(), row.ecdsaAddress())) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_14_ECDSA,
|
||||||
|
"row " + row.index() + ": the ecdsa claim does not recover to 0x"
|
||||||
|
+ hex(row.ecdsaAddress()));
|
||||||
|
}
|
||||||
|
if (!ecdsa.add(Bytes.wrap(row.ecdsaAddress()))) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_14_ECDSA, "row " + row.index() + ": the ecdsa address appears on two rows");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final Bytes32 root = merkleRoot(hashes);
|
||||||
|
final Bytes32 hash = registryHash(chainId, bindHeight, count, root);
|
||||||
|
if (json.has("registryHash")) {
|
||||||
|
final Bytes32 declared = parseBytes32(json.get("registryHash"), "registryHash");
|
||||||
|
if (!declared.equals(hash)) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_16_HASH,
|
||||||
|
"the file declares registryHash " + declared + " and hashes to " + hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Registry(
|
||||||
|
chainId,
|
||||||
|
bindHeight,
|
||||||
|
count,
|
||||||
|
Collections.unmodifiableList(rows),
|
||||||
|
Collections.unmodifiableList(hashes),
|
||||||
|
root,
|
||||||
|
hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================================
|
||||||
|
// Parsing
|
||||||
|
// =============================================================================================
|
||||||
|
|
||||||
|
private static Row parseRow(final int index, final JsonNode e) {
|
||||||
|
final Set<String> allowed = Set.of("id", "keys", "pop", "rotation", "ecdsa", "transport");
|
||||||
|
final Iterator<String> fields = e.fieldNames();
|
||||||
|
while (fields.hasNext()) {
|
||||||
|
final String f = fields.next();
|
||||||
|
if (!allowed.contains(f)) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_03_SHAPE, "row " + index + " has an unknown field '" + f + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final byte[] id = parseHex(e.get("id"), 20, Code.REG3_04_ID, "row " + index + " id");
|
||||||
|
final JsonNode keysNode = e.get("keys");
|
||||||
|
final JsonNode popNode = e.get("pop");
|
||||||
|
if (keysNode == null || !keysNode.isObject() || keysNode.size() == 0) {
|
||||||
|
throw new RegistryV3Exception(Code.REG3_03_SHAPE, "row " + index + " has no keys");
|
||||||
|
}
|
||||||
|
if (popNode == null || !popNode.isObject()) {
|
||||||
|
throw new RegistryV3Exception(Code.REG3_08_POP, "row " + index + " has no pop object");
|
||||||
|
}
|
||||||
|
final TreeMap<Integer, Key> keys = new TreeMap<>();
|
||||||
|
final Iterator<Map.Entry<String, JsonNode>> it = keysNode.fields();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
final Map.Entry<String, JsonNode> k = it.next();
|
||||||
|
final SealScheme scheme =
|
||||||
|
SealSchemes.byId(k.getKey())
|
||||||
|
.orElseThrow(
|
||||||
|
() ->
|
||||||
|
new RegistryV3Exception(
|
||||||
|
Code.REG3_05_UNKNOWN_SCHEME,
|
||||||
|
"row " + index + " names the unknown scheme '" + k.getKey() + "'"));
|
||||||
|
final int len =
|
||||||
|
scheme == SealSchemes.FALCON_512 ? FALCON_512_PK_LENGTH : scheme.publicKeyLength();
|
||||||
|
final byte[] pk =
|
||||||
|
parseHex(k.getValue(), len, Code.REG3_07_KEY_ENCODING, "row " + index + " " + k.getKey());
|
||||||
|
if (scheme == SealSchemes.FALCON_512 && pk[0] != FALCON_512_PK_HEADER) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_07_KEY_ENCODING,
|
||||||
|
"row " + index + " falcon-512: the standard encoding starts with 0x09");
|
||||||
|
}
|
||||||
|
final JsonNode p = popNode.get(k.getKey());
|
||||||
|
if (p == null) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_08_POP, "row " + index + " " + k.getKey() + " has no proof of possession");
|
||||||
|
}
|
||||||
|
final byte[] pop = parseHex(p, -1, Code.REG3_08_POP, "row " + index + " pop " + k.getKey());
|
||||||
|
keys.put(scheme.wireId() & 0xff, new Key(scheme, pk, pop));
|
||||||
|
}
|
||||||
|
if (popNode.size() != keysNode.size()) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_08_POP, "row " + index + " carries a proof for a scheme it has no key for");
|
||||||
|
}
|
||||||
|
byte[] transport = null;
|
||||||
|
final JsonNode t = e.get("transport");
|
||||||
|
if (t != null) {
|
||||||
|
if (!t.isObject() || t.size() != 1 || !t.has("secp256k1Address")) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_15_TRANSPORT, "row " + index + " transport must be {secp256k1Address}");
|
||||||
|
}
|
||||||
|
transport =
|
||||||
|
parseHex(t.get("secp256k1Address"), 20, Code.REG3_15_TRANSPORT, "row " + index + " transport");
|
||||||
|
}
|
||||||
|
final Map<String, byte[]> rotation = new LinkedHashMap<>();
|
||||||
|
final JsonNode rot = e.get("rotation");
|
||||||
|
if (rot != null) {
|
||||||
|
if (!rot.isObject() || rot.size() == 0) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_13_ROTATION_UNEXPECTED, "row " + index + " rotation must be a non-empty map");
|
||||||
|
}
|
||||||
|
final Iterator<Map.Entry<String, JsonNode>> ri = rot.fields();
|
||||||
|
while (ri.hasNext()) {
|
||||||
|
final Map.Entry<String, JsonNode> r = ri.next();
|
||||||
|
if (SealSchemes.byId(r.getKey()).isEmpty()) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_05_UNKNOWN_SCHEME,
|
||||||
|
"row " + index + " rotation names the unknown scheme '" + r.getKey() + "'");
|
||||||
|
}
|
||||||
|
rotation.put(
|
||||||
|
r.getKey(),
|
||||||
|
parseHex(r.getValue(), -1, Code.REG3_12_ROTATION_MISSING, "row " + index + " rotation"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] ecdsaAddress = null;
|
||||||
|
byte[] ecdsaClaim = null;
|
||||||
|
final JsonNode ec = e.get("ecdsa");
|
||||||
|
if (ec != null) {
|
||||||
|
if (!ec.isObject() || ec.size() != 2 || !ec.has("addr") || !ec.has("claim")) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_14_ECDSA, "row " + index + " ecdsa must be {addr, claim}");
|
||||||
|
}
|
||||||
|
ecdsaAddress = parseHex(ec.get("addr"), 20, Code.REG3_14_ECDSA, "row " + index + " ecdsa.addr");
|
||||||
|
ecdsaClaim =
|
||||||
|
parseHex(
|
||||||
|
ec.get("claim"), ECDSA_SIGNATURE_LENGTH, Code.REG3_14_ECDSA, "row " + index + " claim");
|
||||||
|
}
|
||||||
|
return new Row(index, id, keys, transport, rotation, ecdsaAddress, ecdsaClaim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The form a scheme's parser takes: Falcon without the header byte, others as stored. */
|
||||||
|
private static byte[] registryForm(final int index, final Key k) {
|
||||||
|
if (k.scheme() == SealSchemes.FALCON_512) {
|
||||||
|
if (k.publicKey().length != FALCON_512_PK_LENGTH) {
|
||||||
|
throw new RegistryV3Exception(
|
||||||
|
Code.REG3_07_KEY_ENCODING, "row " + index + " falcon-512 key is not 897 bytes");
|
||||||
|
}
|
||||||
|
return Arrays.copyOfRange(k.publicKey(), 1, FALCON_512_PK_LENGTH);
|
||||||
|
}
|
||||||
|
return k.publicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameKeys(final Row a, final Row b) {
|
||||||
|
if (a.keys().size() != b.keys().size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (final Map.Entry<Integer, Key> e : a.keys().entrySet()) {
|
||||||
|
final Key other = b.keys().get(e.getKey());
|
||||||
|
if (other == null || !Arrays.equals(other.publicKey(), e.getValue().publicKey())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Address recover(final Bytes32 digest, final byte[] signature) {
|
||||||
|
if (signature == null || signature.length != ECDSA_SIGNATURE_LENGTH) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final SECPSignature s =
|
||||||
|
SignatureAlgorithmFactory.getInstance().decodeSignature(Bytes.wrap(signature));
|
||||||
|
return Util.signatureToAddress(s, Hash.wrap(digest));
|
||||||
|
} catch (final RuntimeException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] parseHex(
|
||||||
|
final JsonNode n, final int len, final Code code, final String what) {
|
||||||
|
if (n == null || !n.isTextual()) {
|
||||||
|
throw new RegistryV3Exception(code, what + " must be a 0x-prefixed hex string");
|
||||||
|
}
|
||||||
|
final String s = n.asText();
|
||||||
|
if (!s.startsWith("0x") || (s.length() % 2) != 0 || !s.substring(2).matches("[0-9a-f]*")) {
|
||||||
|
throw new RegistryV3Exception(code, what + " is not lowercase 0x-hex of even length");
|
||||||
|
}
|
||||||
|
final byte[] b = Bytes.fromHexString(s).toArrayUnsafe();
|
||||||
|
if (len >= 0 && b.length != len) {
|
||||||
|
throw new RegistryV3Exception(code, what + " is " + b.length + " bytes, expected " + len);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bytes32 parseBytes32(final JsonNode n, final String what) {
|
||||||
|
return Bytes32.wrap(parseHex(n, 32, Code.REG3_16_HASH, what));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bytes32 node(final Bytes32 l, final Bytes32 r) {
|
||||||
|
final byte[] in = new byte[65];
|
||||||
|
in[0] = MERKLE_NODE_TAG;
|
||||||
|
System.arraycopy(l.toArrayUnsafe(), 0, in, 1, 32);
|
||||||
|
System.arraycopy(r.toArrayUnsafe(), 0, in, 33, 32);
|
||||||
|
return Bytes32.wrap(keccak(in));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeAll(final ByteArrayOutputStream out, final byte[] b) {
|
||||||
|
out.write(b, 0, b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] uint32be(final int v) {
|
||||||
|
return new byte[] {(byte) (v >>> 24), (byte) (v >>> 16), (byte) (v >>> 8), (byte) v};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] uint64be(final long v) {
|
||||||
|
final byte[] b = new byte[8];
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
b[i] = (byte) (v >>> (56 - 8 * i));
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] keccak(final byte[] input) {
|
||||||
|
final KeccakDigest d = new KeccakDigest(256);
|
||||||
|
d.update(input, 0, input.length);
|
||||||
|
final byte[] out = new byte[32];
|
||||||
|
d.doFinal(out, 0);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hex(final byte[] b) {
|
||||||
|
final StringBuilder sb = new StringBuilder(b.length * 2);
|
||||||
|
for (final byte x : b) {
|
||||||
|
sb.append(String.format(Locale.ROOT, "%02x", x & 0xff));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,17 @@
|
|||||||
* Changing the mathematics of the chain becomes one property plus keys, never a code edit -
|
* Changing the mathematics of the chain becomes one property plus keys, never a code edit -
|
||||||
* that is the whole point of the abstraction layer.
|
* that is the whole point of the abstraction layer.
|
||||||
*
|
*
|
||||||
|
* PER-SCHEME INTERVALS (AIP-22 stage 2, 2026-09-23). A scheme in a step may carry its own anchor
|
||||||
|
* interval, "scheme/interval": "H:falcon-512/32+slh-dsa-sha2-128s/128" reads "from H, Falcon-512
|
||||||
|
* is required at every anchor height on the 32 grid and SLH-DSA-SHA2-128s only at the anchor
|
||||||
|
* heights on the 128 grid", both grids counted from the chain's anchor activation height H0 (the
|
||||||
|
* one every anchor grid of this chain is counted from, so an anchor of the coarse grid is always
|
||||||
|
* an anchor of the fine one). A scheme without "/interval" is required at EVERY anchor height,
|
||||||
|
* which is exactly the behaviour before this change, so an existing schedule keeps its meaning
|
||||||
|
* byte for byte. The intervals are validated as positive multiples of the anchor grid unit; the
|
||||||
|
* cross-check against the interval the node actually runs is done where both are known
|
||||||
|
* (HybridSealSupport at load), never assumed here.
|
||||||
|
*
|
||||||
* THE D-147 LESSON, APPLIED AT THE LOADER. The min-seals schedule once accepted a shape whose
|
* THE D-147 LESSON, APPLIED AT THE LOADER. The min-seals schedule once accepted a shape whose
|
||||||
* DANGEROUS step was later in the schedule, because validation looked only at the first step.
|
* DANGEROUS step was later in the schedule, because validation looked only at the first step.
|
||||||
* Here every rule runs over the WHOLE schedule at parse time, and the armability gate
|
* Here every rule runs over the WHOLE schedule at parse time, and the armability gate
|
||||||
@ -23,16 +34,42 @@ package org.hyperledger.besu.consensus.common.bft;
|
|||||||
import com.google.common.base.Splitter;
|
import com.google.common.base.Splitter;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/** Immutable, validated height-to-scheme-set schedule. */
|
/** Immutable, validated height-to-scheme-set schedule. */
|
||||||
public final class PqSchemeSchedule {
|
public final class PqSchemeSchedule {
|
||||||
|
|
||||||
/** One step: from {@code fromBlock} (inclusive) the anchor carries {@code schemeIds}. */
|
/**
|
||||||
public record Step(long fromBlock, Set<String> schemeIds) {}
|
* The unit every anchor grid of an Aere chain is a multiple of (the base anchor interval of both
|
||||||
|
* chain 2800 and testnet 28001). A per-scheme interval that is not a multiple of it could never
|
||||||
|
* land on anchor heights, so it is refused at parse time.
|
||||||
|
*/
|
||||||
|
public static final int GRID_UNIT = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One step: from {@code fromBlock} (inclusive) the anchor carries {@code schemeIds}; a scheme
|
||||||
|
* present in {@code intervals} is required only at anchor heights on that interval's grid,
|
||||||
|
* a scheme absent from it at every anchor height.
|
||||||
|
*/
|
||||||
|
public record Step(long fromBlock, Set<String> schemeIds, Map<String, Integer> intervals) {
|
||||||
|
/** Canonical form: defensive copies, never null. */
|
||||||
|
public Step(
|
||||||
|
final long fromBlock, final Set<String> schemeIds, final Map<String, Integer> intervals) {
|
||||||
|
this.fromBlock = fromBlock;
|
||||||
|
this.schemeIds = Set.copyOf(schemeIds);
|
||||||
|
this.intervals = intervals == null ? Map.of() : Map.copyOf(intervals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A step whose schemes are all required at every anchor height (the pre-AIP-22 form). */
|
||||||
|
public Step(final long fromBlock, final Set<String> schemeIds) {
|
||||||
|
this(fromBlock, schemeIds, Map.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final List<Step> steps;
|
private final List<Step> steps;
|
||||||
|
|
||||||
@ -40,8 +77,9 @@ public final class PqSchemeSchedule {
|
|||||||
this.steps = steps;
|
this.steps = steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parse "H:scheme[+scheme...],H:...". Refuses the WHOLE schedule on any defect: unknown or
|
/** Parse "H:scheme[/interval][+scheme[/interval]...],H:...". Refuses the WHOLE schedule on any
|
||||||
* repeated scheme in a step, empty step, non-increasing heights, negative height, garbage. */
|
* defect: unknown or repeated scheme in a step, empty step, non-increasing heights, negative
|
||||||
|
* height, an interval that is not a positive multiple of {@link #GRID_UNIT}, garbage. */
|
||||||
public static PqSchemeSchedule parse(final String raw) {
|
public static PqSchemeSchedule parse(final String raw) {
|
||||||
if (raw == null || raw.isBlank()) {
|
if (raw == null || raw.isBlank()) {
|
||||||
throw new IllegalArgumentException("AERE PQ ORAR-SCHEME: empty schedule");
|
throw new IllegalArgumentException("AERE PQ ORAR-SCHEME: empty schedule");
|
||||||
@ -74,12 +112,15 @@ public final class PqSchemeSchedule {
|
|||||||
}
|
}
|
||||||
lastHeight = h;
|
lastHeight = h;
|
||||||
final Set<String> schemes = new LinkedHashSet<>();
|
final Set<String> schemes = new LinkedHashSet<>();
|
||||||
|
final Map<String, Integer> intervals = new LinkedHashMap<>();
|
||||||
for (final String id : Splitter.on('+').split(parti.get(1))) {
|
for (final String id : Splitter.on('+').split(parti.get(1))) {
|
||||||
final String curat = id.trim();
|
final String piece = id.trim();
|
||||||
if (curat.isEmpty()) {
|
if (piece.isEmpty()) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"AERE PQ ORAR-SCHEME: step at " + h + " carries an empty scheme name");
|
"AERE PQ ORAR-SCHEME: step at " + h + " carries an empty scheme name");
|
||||||
}
|
}
|
||||||
|
final int slash = piece.indexOf('/');
|
||||||
|
final String curat = slash < 0 ? piece : piece.substring(0, slash).trim();
|
||||||
if (SealSchemes.byId(curat).isEmpty()) {
|
if (SealSchemes.byId(curat).isEmpty()) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"AERE PQ ORAR-SCHEME: step at " + h + " names UNKNOWN scheme '" + curat
|
"AERE PQ ORAR-SCHEME: step at " + h + " names UNKNOWN scheme '" + curat
|
||||||
@ -89,23 +130,41 @@ public final class PqSchemeSchedule {
|
|||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"AERE PQ ORAR-SCHEME: step at " + h + " repeats scheme '" + curat + "'");
|
"AERE PQ ORAR-SCHEME: step at " + h + " repeats scheme '" + curat + "'");
|
||||||
}
|
}
|
||||||
|
if (slash >= 0) {
|
||||||
|
final String rawInterval = piece.substring(slash + 1).trim();
|
||||||
|
final int interval;
|
||||||
|
try {
|
||||||
|
interval = Integer.parseInt(rawInterval);
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE PQ ORAR-SCHEME: step at " + h + " gives scheme '" + curat
|
||||||
|
+ "' the interval '" + rawInterval + "', which is not a number");
|
||||||
|
}
|
||||||
|
if (interval <= 0 || interval % GRID_UNIT != 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE PQ ORAR-SCHEME: step at " + h + " gives scheme '" + curat
|
||||||
|
+ "' the interval " + interval + ", which is not a positive multiple of the"
|
||||||
|
+ " anchor grid unit " + GRID_UNIT + " - such a scheme could never land on an"
|
||||||
|
+ " anchor height");
|
||||||
|
}
|
||||||
|
intervals.put(curat, interval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (schemes.isEmpty()) {
|
if (schemes.isEmpty()) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"AERE PQ ORAR-SCHEME: step at " + h + " has no schemes at all");
|
"AERE PQ ORAR-SCHEME: step at " + h + " has no schemes at all");
|
||||||
}
|
}
|
||||||
steps.add(new Step(h, Set.copyOf(schemes)));
|
steps.add(new Step(h, schemes, intervals));
|
||||||
}
|
}
|
||||||
return new PqSchemeSchedule(List.copyOf(steps));
|
return new PqSchemeSchedule(List.copyOf(steps));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The scheme set in force at {@code height}: the last step at or below it, or the empty set
|
/** The step in force at {@code height}, or null before the first one. */
|
||||||
* when the schedule has not started yet (= v2 not armed, legacy governs). */
|
private Step stepAt(final long height) {
|
||||||
public Set<String> schemesAt(final long height) {
|
Step inForce = null;
|
||||||
Set<String> inForce = Set.of();
|
|
||||||
for (final Step s : steps) {
|
for (final Step s : steps) {
|
||||||
if (s.fromBlock() <= height) {
|
if (s.fromBlock() <= height) {
|
||||||
inForce = s.schemeIds();
|
inForce = s;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -113,6 +172,57 @@ public final class PqSchemeSchedule {
|
|||||||
return inForce;
|
return inForce;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The scheme set in force at {@code height}: the last step at or below it, or the empty set
|
||||||
|
* when the schedule has not started yet (= v2 not armed, legacy governs). This is the set a
|
||||||
|
* node must hold KEYS for and the set the armability gate checks; which of them an anchor
|
||||||
|
* actually demands is {@link #schemesRequiredForAnchor}. */
|
||||||
|
public Set<String> schemesAt(final long height) {
|
||||||
|
final Step s = stepAt(height);
|
||||||
|
return s == null ? Set.of() : s.schemeIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The schemes an anchor at {@code anchorHeight} must carry, given that its scheme step is read
|
||||||
|
* at the PARENT height (the seals of a certificate are made when the parent is committed, so
|
||||||
|
* every consumer of this schedule already indexes it by the parent), and that a scheme with a
|
||||||
|
* per-scheme interval is required only when {@code (anchorHeight - anchorBlock)} is a multiple
|
||||||
|
* of that interval. Without per-scheme intervals this is exactly {@code schemesAt(anchorHeight
|
||||||
|
* - 1)}.
|
||||||
|
*
|
||||||
|
* @param anchorHeight the anchor height being produced, enforced or validated
|
||||||
|
* @param anchorBlock the chain's anchor activation height, the origin of every anchor grid
|
||||||
|
* @return the required scheme ids, possibly a strict subset of the step's schemes
|
||||||
|
*/
|
||||||
|
public Set<String> schemesRequiredForAnchor(final long anchorHeight, final long anchorBlock) {
|
||||||
|
final Step s = stepAt(anchorHeight - 1);
|
||||||
|
if (s == null) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
if (s.intervals().isEmpty()) {
|
||||||
|
return s.schemeIds();
|
||||||
|
}
|
||||||
|
final Set<String> required = new LinkedHashSet<>();
|
||||||
|
for (final String id : s.schemeIds()) {
|
||||||
|
final Integer interval = s.intervals().get(id);
|
||||||
|
if (interval == null || onGrid(anchorHeight, anchorBlock, interval)) {
|
||||||
|
required.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Set.copyOf(required);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean onGrid(final long height, final long origin, final int interval) {
|
||||||
|
if (height < origin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (height - origin) % interval == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether any step carries a per-scheme interval (then the grid cross-check applies). */
|
||||||
|
public boolean hasIntervals() {
|
||||||
|
return steps.stream().anyMatch(s -> !s.intervals().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
/** All steps, ascending. */
|
/** All steps, ascending. */
|
||||||
public List<Step> steps() {
|
public List<Step> steps() {
|
||||||
return steps;
|
return steps;
|
||||||
|
|||||||
@ -23,6 +23,7 @@ package org.hyperledger.besu.consensus.common.bft.blockcreation;
|
|||||||
import static com.google.common.base.Preconditions.checkState;
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
|
||||||
import org.hyperledger.besu.config.BftConfigOptions;
|
import org.hyperledger.besu.config.BftConfigOptions;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
|
||||||
import org.hyperledger.besu.consensus.common.ConsensusHelpers;
|
import org.hyperledger.besu.consensus.common.ConsensusHelpers;
|
||||||
import org.hyperledger.besu.consensus.common.ForksSchedule;
|
import org.hyperledger.besu.consensus.common.ForksSchedule;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
||||||
@ -119,7 +120,8 @@ public class BftBlockCreatorFactory<T extends BftConfigOptions> {
|
|||||||
return new BftBlockCreator(
|
return new BftBlockCreator(
|
||||||
miningConfiguration,
|
miningConfiguration,
|
||||||
forksSchedule,
|
forksSchedule,
|
||||||
localAddress,
|
AerePqIdentity.local(
|
||||||
|
localAddress, protocolContext.getBlockchain().getChainHeadBlockNumber() + 1L),
|
||||||
ph -> createExtraData(round, ph),
|
ph -> createExtraData(round, ph),
|
||||||
transactionPool,
|
transactionPool,
|
||||||
protocolContext,
|
protocolContext,
|
||||||
@ -186,7 +188,10 @@ public class BftBlockCreatorFactory<T extends BftConfigOptions> {
|
|||||||
validatorProvider.getVoteProviderAfterBlock(parentHeader);
|
validatorProvider.getVoteProviderAfterBlock(parentHeader);
|
||||||
checkState(voteProviderAfterBlock.isPresent(), "Bft requires a vote provider");
|
checkState(voteProviderAfterBlock.isPresent(), "Bft requires a vote provider");
|
||||||
final Optional<ValidatorVote> proposal =
|
final Optional<ValidatorVote> proposal =
|
||||||
voteProviderAfterBlock.get().getVoteAfterBlock(parentHeader, localAddress);
|
voteProviderAfterBlock
|
||||||
|
.get()
|
||||||
|
.getVoteAfterBlock(
|
||||||
|
parentHeader, AerePqIdentity.local(localAddress, parentHeader.getNumber() + 1L));
|
||||||
|
|
||||||
final List<Address> validators =
|
final List<Address> validators =
|
||||||
new ArrayList<>(validatorProvider.getValidatorsAfterBlock(parentHeader));
|
new ArrayList<>(validatorProvider.getValidatorsAfterBlock(parentHeader));
|
||||||
|
|||||||
@ -16,6 +16,7 @@ package org.hyperledger.besu.consensus.common.bft.blockcreation;
|
|||||||
|
|
||||||
import org.apache.tuweni.bytes.Bytes;
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
import org.hyperledger.besu.datatypes.Hash;
|
import org.hyperledger.besu.datatypes.Hash;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
||||||
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
||||||
@ -380,8 +381,14 @@ public final class PqAnchorProducer {
|
|||||||
falconIndices.add(f.getValidatorIndex());
|
falconIndices.add(f.getValidatorIndex());
|
||||||
}
|
}
|
||||||
final HybridSealSupport hybrid = HybridSealSupport.instance();
|
final HybridSealSupport hybrid = HybridSealSupport.instance();
|
||||||
|
// AIP-22 stage 2: only the schemes THIS anchor height demands (per-scheme grids counted from
|
||||||
|
// the activation height); at a Falcon-only anchor no SLH-DSA seal is gathered or written, which
|
||||||
|
// is the whole disk saving of the two-grid design
|
||||||
final Set<String> schemes =
|
final Set<String> schemes =
|
||||||
hybrid.schedule().map(sch -> sch.schemesAt(parentNumber)).orElse(Set.of());
|
hybrid
|
||||||
|
.schedule()
|
||||||
|
.map(sch -> sch.schemesRequiredForAnchor(blockNumber, cfg.anchorBlock()))
|
||||||
|
.orElse(Set.of());
|
||||||
final List<SchemeSeal> heard =
|
final List<SchemeSeal> heard =
|
||||||
PqSealCache.instance().extrasFor(parentNumber, parentHeader.getHash());
|
PqSealCache.instance().extrasFor(parentNumber, parentHeader.getHash());
|
||||||
for (final String schemeId : schemes) {
|
for (final String schemeId : schemes) {
|
||||||
@ -397,7 +404,9 @@ public final class PqAnchorProducer {
|
|||||||
"the scheme schedule names '" + schemeId + "', which this binary does not implement");
|
"the scheme schedule names '" + schemeId + "', which this binary does not implement");
|
||||||
}
|
}
|
||||||
final Optional<HybridSignerRegistry> registry = hybrid.registry();
|
final Optional<HybridSignerRegistry> registry = hybrid.registry();
|
||||||
if (registry.isEmpty()) {
|
// AERE AIP-22 stage 3b: from H_k the keys come from the v3 epoch in force at the parent
|
||||||
|
final boolean v3Keys = AerePqIdentity.keysFromV3(parentNumber);
|
||||||
|
if (registry.isEmpty() && !v3Keys) {
|
||||||
throw new PqAnchorNotReadyException(
|
throw new PqAnchorNotReadyException(
|
||||||
parentNumber,
|
parentNumber,
|
||||||
0,
|
0,
|
||||||
@ -414,7 +423,10 @@ public final class PqAnchorProducer {
|
|||||||
rejected++;
|
rejected++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final Optional<byte[]> pk = registry.get().publicKey(s.getValidatorIndex(), schemeId);
|
final Optional<byte[]> pk =
|
||||||
|
v3Keys
|
||||||
|
? AerePqIdentity.v3KeyAt(parentNumber, s.getValidatorIndex(), schemeId)
|
||||||
|
: registry.get().publicKey(s.getValidatorIndex(), schemeId);
|
||||||
if (pk.isEmpty()
|
if (pk.isEmpty()
|
||||||
|| !scheme.get().verifyRaw(pk.get(), message.toArray(), s.getSignature().toArray())) {
|
|| !scheme.get().verifyRaw(pk.get(), message.toArray(), s.getSignature().toArray())) {
|
||||||
rejected++;
|
rejected++;
|
||||||
|
|||||||
@ -0,0 +1,288 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3: the tool that makes a registry v3 epoch FROM THE KEYS ALREADY ON THE VALIDATORS.
|
||||||
|
*
|
||||||
|
* Two commands, and the split is the security property:
|
||||||
|
*
|
||||||
|
* row runs ON THE HOST that holds a validator's keys. It reads that validator's Falcon-512 key
|
||||||
|
* file (the same "index, f, g, F, pk" properties the node loads through aere.falcon.key),
|
||||||
|
* its SLH-DSA-SHA2-128s key file (the same "index, sk" the hybrid loader reads) and its ECDSA
|
||||||
|
* validator key (a file holding the 32-byte hex), and prints ONE row: the id derived from the
|
||||||
|
* Falcon key, both public keys in their standard encodings, a proof of possession by each, and
|
||||||
|
* the ecdsa link signed by the validator key. It prints no private key, nor any part of one.
|
||||||
|
* assemble runs anywhere (the laptop): it takes the rows, builds the manifest, LOADS it through the
|
||||||
|
* strict loader (every proof and claim is verified again) and prints the manifest and its
|
||||||
|
* registry hash - the value the chain pins.
|
||||||
|
*
|
||||||
|
* Nothing leaves a host but signatures and public keys, which is what "no new key material and nothing
|
||||||
|
* out of the vault" means for this ceremony (AIP-22 section 3.2, the plan's stage 3).
|
||||||
|
*
|
||||||
|
* java -cp '<besu lib>/*' org.hyperledger.besu.consensus.common.bft.tools.PqRegistryV3Tool row \
|
||||||
|
* --chain-id 28001 --bind H --count N --index i --falcon-key F --slh-key S --slh-pub HEX \
|
||||||
|
* --ecdsa-key-file K [--transport]
|
||||||
|
* java -cp '<besu lib>/*' ...PqRegistryV3Tool assemble --chain-id 28001 --bind H --count N \
|
||||||
|
* --required falcon-512,slh-dsa-sha2-128s row0.json row1.json ...
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.tools;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.FalconSealScheme;
|
||||||
|
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.crypto.KeyPair;
|
||||||
|
import org.hyperledger.besu.crypto.SECPPrivateKey;
|
||||||
|
import org.hyperledger.besu.crypto.SignatureAlgorithm;
|
||||||
|
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
|
||||||
|
import org.hyperledger.besu.ethereum.core.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.bouncycastle.pqc.crypto.falcon.FalconParameters;
|
||||||
|
import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
|
||||||
|
|
||||||
|
/** The registry v3 ceremony tool. */
|
||||||
|
public final class PqRegistryV3Tool {
|
||||||
|
|
||||||
|
private static final ObjectMapper M = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||||
|
|
||||||
|
private PqRegistryV3Tool() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point.
|
||||||
|
*
|
||||||
|
* @param args the command and its options
|
||||||
|
* @throws IOException on unreadable input
|
||||||
|
*/
|
||||||
|
public static void main(final String[] args) throws IOException {
|
||||||
|
if (args.length == 0) {
|
||||||
|
usage();
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
final Map<String, String> o = new HashMap<>();
|
||||||
|
final List<String> files = new ArrayList<>();
|
||||||
|
for (int i = 1; i < args.length; i++) {
|
||||||
|
if (args[i].startsWith("--")) {
|
||||||
|
if (args[i].equals("--transport")) {
|
||||||
|
o.put("transport", "1");
|
||||||
|
} else if (i + 1 < args.length) {
|
||||||
|
o.put(args[i].substring(2), args[++i]);
|
||||||
|
} else {
|
||||||
|
usage();
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
files.add(args[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (args[0]) {
|
||||||
|
case "keygen" -> keygen(o);
|
||||||
|
case "row" -> row(o);
|
||||||
|
case "assemble" -> assemble(o, files);
|
||||||
|
default -> {
|
||||||
|
usage();
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AIP-22 stage 3b: a NEW key pair for a validator, generated ON THE HOST that will hold it (a rotation). Writes the Falcon-512
|
||||||
|
* key file (index, f, g, F, pk - the format aere.falcon.key and aere.falcon.keys.next read) and the SLH-DSA-SHA2-128s key file
|
||||||
|
* (index, alg, sk, pk - the format the hybrid loaders read), owner-only, refusing to overwrite anything; prints only the public
|
||||||
|
* keys.
|
||||||
|
*/
|
||||||
|
private static void keygen(final Map<String, String> o) throws IOException {
|
||||||
|
final int index = Integer.parseInt(need(o, "index"));
|
||||||
|
final Path fOut = Path.of(need(o, "out-falcon"));
|
||||||
|
final Path sOut = Path.of(need(o, "out-slh"));
|
||||||
|
if (Files.exists(fOut) || Files.exists(sOut)) {
|
||||||
|
throw new IllegalStateException("refusing to overwrite an existing key file");
|
||||||
|
}
|
||||||
|
final java.security.SecureRandom rng =
|
||||||
|
org.hyperledger.besu.crypto.SecureRandomProvider.createSecureRandom();
|
||||||
|
final org.bouncycastle.pqc.crypto.falcon.FalconKeyPairGenerator g =
|
||||||
|
new org.bouncycastle.pqc.crypto.falcon.FalconKeyPairGenerator();
|
||||||
|
g.init(new org.bouncycastle.pqc.crypto.falcon.FalconKeyGenerationParameters(rng, FalconParameters.falcon_512));
|
||||||
|
final FalconPrivateKeyParameters fk = (FalconPrivateKeyParameters) g.generateKeyPair().getPrivate();
|
||||||
|
final SealScheme slh = SealSchemes.SLH_DSA_128S;
|
||||||
|
final SealScheme.GeneratedPair sp = slh.generate(rng);
|
||||||
|
final byte[] slhSk = slh.serializePrivateKey(sp.privateKey()).orElseThrow();
|
||||||
|
final String falconFile =
|
||||||
|
"index=" + index + "\nf=" + hx(fk.getSpolyf()).substring(2) + "\ng=" + hx(fk.getG()).substring(2)
|
||||||
|
+ "\nF=" + hx(fk.getSpolyF()).substring(2) + "\npk=" + hx(fk.getPublicKey()).substring(2) + "\n";
|
||||||
|
final String slhFile =
|
||||||
|
"index=" + index + "\nalg=slh-dsa-sha2-128s\nsk=" + hx(slhSk).substring(2) + "\npk="
|
||||||
|
+ hx(sp.publicRegistryForm()).substring(2) + "\n";
|
||||||
|
final java.nio.file.attribute.FileAttribute<?> ownerOnly =
|
||||||
|
java.nio.file.attribute.PosixFilePermissions.asFileAttribute(
|
||||||
|
java.nio.file.attribute.PosixFilePermissions.fromString("rw-------"));
|
||||||
|
Files.writeString(Files.createFile(fOut, ownerOnly), falconFile, StandardCharsets.UTF_8);
|
||||||
|
Files.writeString(Files.createFile(sOut, ownerOnly), slhFile, StandardCharsets.UTF_8);
|
||||||
|
final ObjectNode out = M.createObjectNode();
|
||||||
|
out.put("index", index);
|
||||||
|
out.put("falconPk", hx(fk.getPublicKey()));
|
||||||
|
out.put("slhPub", hx(sp.publicRegistryForm()));
|
||||||
|
System.out.println(M.writeValueAsString(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void row(final Map<String, String> o) throws IOException {
|
||||||
|
final long chain = Long.parseLong(need(o, "chain-id"));
|
||||||
|
final long bind = Long.parseLong(need(o, "bind"));
|
||||||
|
final int count = Integer.parseInt(need(o, "count"));
|
||||||
|
final int index = Integer.parseInt(need(o, "index"));
|
||||||
|
// Falcon: the node's own key file; the index inside must be the index asked for
|
||||||
|
final Properties fp = new Properties();
|
||||||
|
fp.load(new StringReader(Files.readString(Path.of(need(o, "falcon-key")), StandardCharsets.UTF_8)));
|
||||||
|
if (Integer.parseInt(fp.getProperty("index").trim()) != index) {
|
||||||
|
throw new IllegalArgumentException("the Falcon key file is for index " + fp.getProperty("index"));
|
||||||
|
}
|
||||||
|
final byte[] h = hex(fp.getProperty("pk"));
|
||||||
|
final FalconPrivateKeyParameters fsk =
|
||||||
|
new FalconPrivateKeyParameters(
|
||||||
|
FalconParameters.falcon_512, hex(fp.getProperty("f")), hex(fp.getProperty("g")),
|
||||||
|
hex(fp.getProperty("F")), h);
|
||||||
|
final byte[] falconStd = new byte[PqRegistryV3.FALCON_512_PK_LENGTH];
|
||||||
|
falconStd[0] = PqRegistryV3.FALCON_512_PK_HEADER;
|
||||||
|
System.arraycopy(h, 0, falconStd, 1, h.length);
|
||||||
|
// AIP-22 stage 3b: a ROTATED row keeps the id of its first registration (--id); a new row's id is its Falcon key's derivation
|
||||||
|
final byte[] id = o.containsKey("id") ? hex(o.get("id")) : PqRegistryV3.idOf(falconStd);
|
||||||
|
final byte[] falconPop =
|
||||||
|
falconSign(fsk, PqRegistryV3.popMessage(chain, bind, count, index, id, SealSchemes.FALCON_512.wireId(), falconStd));
|
||||||
|
// SLH-DSA: the hybrid key file; the public key is given (it is the registry's), and the private key
|
||||||
|
// must sign a probe that verifies under it before anything is printed
|
||||||
|
final Properties sp = new Properties();
|
||||||
|
sp.load(new StringReader(Files.readString(Path.of(need(o, "slh-key")), StandardCharsets.UTF_8)));
|
||||||
|
if (Integer.parseInt(sp.getProperty("index").trim()) != index) {
|
||||||
|
throw new IllegalArgumentException("the SLH-DSA key file is for index " + sp.getProperty("index"));
|
||||||
|
}
|
||||||
|
final SealScheme slh = SealSchemes.SLH_DSA_128S;
|
||||||
|
final SealScheme.PrivateHandle ssk =
|
||||||
|
slh.parsePrivateKey(hex(sp.getProperty("sk"))).orElseThrow(
|
||||||
|
() -> new IllegalArgumentException("the SLH-DSA key does not parse"));
|
||||||
|
final byte[] slhPub = hex(need(o, "slh-pub"));
|
||||||
|
final byte[] slhPop =
|
||||||
|
slh.sign(ssk, PqRegistryV3.popMessage(chain, bind, count, index, id, slh.wireId(), slhPub).toArrayUnsafe())
|
||||||
|
.orElseThrow();
|
||||||
|
if (!slh.verifyRaw(slhPub, PqRegistryV3.popMessage(chain, bind, count, index, id, slh.wireId(), slhPub).toArrayUnsafe(), slhPop)) {
|
||||||
|
throw new IllegalStateException("the SLH-DSA key file does not match the public key given; refusing");
|
||||||
|
}
|
||||||
|
// the ECDSA validator key: signs the link from its address to the row
|
||||||
|
final SignatureAlgorithm ec = SignatureAlgorithmFactory.getInstance();
|
||||||
|
final String ecHex = Files.readString(Path.of(need(o, "ecdsa-key-file")), StandardCharsets.UTF_8).trim();
|
||||||
|
final SECPPrivateKey ecsk = ec.createPrivateKey(Bytes32.fromHexString(ecHex));
|
||||||
|
final KeyPair kp = ec.createKeyPair(ecsk);
|
||||||
|
final byte[] addr = Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe();
|
||||||
|
final TreeMap<Integer, PqRegistryV3.Key> keys = new TreeMap<>();
|
||||||
|
keys.put(SealSchemes.FALCON_512.wireId() & 0xff, new PqRegistryV3.Key(SealSchemes.FALCON_512, falconStd, falconPop));
|
||||||
|
keys.put(slh.wireId() & 0xff, new PqRegistryV3.Key(slh, slhPub, slhPop));
|
||||||
|
final byte[] transport = o.containsKey("transport") ? addr : null;
|
||||||
|
final Bytes32 rh =
|
||||||
|
PqRegistryV3.rowHash(chain, bind, count, new PqRegistryV3.Row(index, id, keys, transport, Map.of(), null, null));
|
||||||
|
final byte[] claim = ec.sign(PqRegistryV3.claimDigest(chain, rh), kp).encodedBytes().toArrayUnsafe();
|
||||||
|
final ObjectNode row = M.createObjectNode();
|
||||||
|
row.put("id", hx(id));
|
||||||
|
row.putObject("keys").put("falcon-512", hx(falconStd)).put("slh-dsa-sha2-128s", hx(slhPub));
|
||||||
|
row.putObject("pop").put("falcon-512", hx(falconPop)).put("slh-dsa-sha2-128s", hx(slhPop));
|
||||||
|
if (transport != null) {
|
||||||
|
row.putObject("transport").put("secp256k1Address", hx(transport));
|
||||||
|
}
|
||||||
|
row.putObject("ecdsa").put("addr", hx(addr)).put("claim", hx(claim));
|
||||||
|
// AIP-22 stage 3b: a rotation - EACH old key of the row signs the new row's hash (AERE-PQ-ROTATE-3); the loader requires
|
||||||
|
// one signature per scheme the previous epoch bound, by exactly that previous key
|
||||||
|
if (o.containsKey("old-falcon-key") || o.containsKey("old-slh-key")) {
|
||||||
|
final Bytes32 rot = PqRegistryV3.rotateMessage(rh);
|
||||||
|
final Properties of = new Properties();
|
||||||
|
of.load(new StringReader(Files.readString(Path.of(need(o, "old-falcon-key")), StandardCharsets.UTF_8)));
|
||||||
|
final FalconPrivateKeyParameters oldF =
|
||||||
|
new FalconPrivateKeyParameters(
|
||||||
|
FalconParameters.falcon_512, hex(of.getProperty("f")), hex(of.getProperty("g")),
|
||||||
|
hex(of.getProperty("F")), hex(of.getProperty("pk")));
|
||||||
|
final Properties os = new Properties();
|
||||||
|
os.load(new StringReader(Files.readString(Path.of(need(o, "old-slh-key")), StandardCharsets.UTF_8)));
|
||||||
|
final SealScheme.PrivateHandle oldS =
|
||||||
|
slh.parsePrivateKey(hex(os.getProperty("sk"))).orElseThrow(
|
||||||
|
() -> new IllegalArgumentException("the old SLH-DSA key does not parse"));
|
||||||
|
row.putObject("rotation")
|
||||||
|
.put("falcon-512", hx(falconSign(oldF, rot)))
|
||||||
|
.put("slh-dsa-sha2-128s", hx(slh.sign(oldS, rot.toArrayUnsafe()).orElseThrow()));
|
||||||
|
}
|
||||||
|
final ObjectNode out = M.createObjectNode();
|
||||||
|
out.put("index", index);
|
||||||
|
out.set("row", row);
|
||||||
|
System.out.println(M.writeValueAsString(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assemble(final Map<String, String> o, final List<String> files) throws IOException {
|
||||||
|
final long chain = Long.parseLong(need(o, "chain-id"));
|
||||||
|
final long bind = Long.parseLong(need(o, "bind"));
|
||||||
|
final int count = Integer.parseInt(need(o, "count"));
|
||||||
|
final List<String> required = Arrays.asList(o.getOrDefault("required", "falcon-512").split(","));
|
||||||
|
final ObjectNode m = M.createObjectNode();
|
||||||
|
m.put("formatVersion", PqRegistryV3.FORMAT_VERSION);
|
||||||
|
m.put("chainId", chain);
|
||||||
|
m.put("bindHeight", bind);
|
||||||
|
m.put("count", count);
|
||||||
|
for (final String f : files) {
|
||||||
|
final JsonNode r = M.readTree(Files.readString(Path.of(f), StandardCharsets.UTF_8));
|
||||||
|
m.set(Integer.toString(r.get("index").asInt()), r.get("row"));
|
||||||
|
}
|
||||||
|
// AIP-22 stage 3b: an epoch after the switch epoch is loaded AGAINST the one before it (--previous), so every rotation it
|
||||||
|
// carries is verified; the switch epoch itself (no --previous) must carry every ecdsa link
|
||||||
|
PqRegistryV3.Registry previous = null;
|
||||||
|
if (o.containsKey("previous")) {
|
||||||
|
final JsonNode pj = M.readTree(Files.readString(Path.of(o.get("previous")), StandardCharsets.UTF_8));
|
||||||
|
previous = PqRegistryV3.load(pj, chain, required, null, true);
|
||||||
|
}
|
||||||
|
final PqRegistryV3.Registry reg = PqRegistryV3.load(m, chain, required, previous, previous == null);
|
||||||
|
m.put("registryHash", reg.hash().toHexString());
|
||||||
|
System.out.println(M.writeValueAsString(m));
|
||||||
|
System.err.println("registryHash " + reg.hash().toHexString() + " (" + count + " rows, bound at " + bind + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] falconSign(final FalconPrivateKeyParameters sk, final Bytes32 message) {
|
||||||
|
return ((FalconSealScheme) SealSchemes.FALCON_512)
|
||||||
|
.signWithParams(sk, message.toArrayUnsafe())
|
||||||
|
.orElseThrow(() -> new IllegalStateException("the Falcon key did not sign"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String need(final Map<String, String> o, final String k) {
|
||||||
|
final String v = o.get(k);
|
||||||
|
if (v == null) {
|
||||||
|
throw new IllegalArgumentException("missing --" + k);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] hex(final String s) {
|
||||||
|
return Bytes.fromHexStringLenient(s.trim()).toArrayUnsafe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hx(final byte[] b) {
|
||||||
|
final StringBuilder sb = new StringBuilder("0x");
|
||||||
|
for (final byte x : b) {
|
||||||
|
sb.append(String.format(Locale.ROOT, "%02x", x & 0xff));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void usage() {
|
||||||
|
System.err.println("usage: PqRegistryV3Tool row|assemble ... (see the class comment)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3b: probes of the keys that seal coming from the v3 epoch in force (AerePqIdentity
|
||||||
|
* keysFromV3 / v3KeyAt / v3IdAt / verifyKeyContinuity) and of the ecdsa link inherited by id, on the two
|
||||||
|
* shared registry v3 conformance vectors: ok-epoch-a (bound at 1000, three rows, each with its ecdsa link)
|
||||||
|
* and ok-epoch-b-rotation (bound at 2000: index 1 rotates both keys under its id, the id of A's index 2
|
||||||
|
* moves to index 0, A's index 0 leaves, a new id joins at index 2; no ecdsa links).
|
||||||
|
*
|
||||||
|
* What must hold, each with its negative: H_k falls on an epoch boundary at or above H_id or the node
|
||||||
|
* refuses to arm; below H_k nothing comes from v3; from H_k the key of an index is the key of THAT index in
|
||||||
|
* the epoch in force (Falcon without its 0x09 header, SLH-DSA as its 32 bytes), so the rotated key replaces
|
||||||
|
* the old one exactly at the rotation epoch and an index the epoch does not have gives NO key; the
|
||||||
|
* continuity guard accepts the same keys and refuses one changed byte or a missing key; and an author keeps
|
||||||
|
* its id across an epoch that omits the ecdsa links, but a removed id's address authors as nothing.
|
||||||
|
*/
|
||||||
|
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.datatypes.Address;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
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.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class AereKeysFromV3Test {
|
||||||
|
|
||||||
|
private static final List<String> SCHEMES = List.of("falcon-512", "slh-dsa-sha2-128s");
|
||||||
|
private PqRegistryV3.Registry a;
|
||||||
|
private PqRegistryV3.Registry b;
|
||||||
|
private TreeMap<Long, PqRegistryV3.Registry> epochs;
|
||||||
|
|
||||||
|
private JsonNode vector(final String name) throws IOException {
|
||||||
|
try (InputStream in = getClass().getResourceAsStream("/aere/registry-v3/" + name + ".json")) {
|
||||||
|
assertThat(in).isNotNull();
|
||||||
|
return new ObjectMapper().readTree(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void load() throws IOException {
|
||||||
|
final JsonNode va = vector("ok-epoch-a");
|
||||||
|
final JsonNode vb = vector("ok-epoch-b-rotation");
|
||||||
|
a = PqRegistryV3.load(va.get("manifest"), va.get("chainId").asLong(), SCHEMES, null, true);
|
||||||
|
b = PqRegistryV3.load(vb.get("manifest"), vb.get("chainId").asLong(), SCHEMES, a, false);
|
||||||
|
assertThat(a.bindHeight()).isEqualTo(1000L);
|
||||||
|
assertThat(b.bindHeight()).isEqualTo(2000L);
|
||||||
|
epochs = new TreeMap<>();
|
||||||
|
epochs.put(a.bindHeight(), a);
|
||||||
|
epochs.put(b.bindHeight(), b);
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void disarm() {
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] key(final PqRegistryV3.Registry r, final int index, final String scheme) {
|
||||||
|
return r.rows().get(index).key(scheme).orElseThrow().publicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Address id(final PqRegistryV3.Registry r, final int index) {
|
||||||
|
return Address.wrap(Bytes.wrap(r.rows().get(index).id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Address ecdsa(final PqRegistryV3.Registry r, final int index) {
|
||||||
|
return Address.wrap(Bytes.wrap(r.rows().get(index).ecdsaAddress()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void keysFromMustBeAnEpochBoundaryAtOrAboveTheIdentityHeight() {
|
||||||
|
assertThatThrownBy(() -> AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs, 1999)))
|
||||||
|
.isInstanceOf(AerePqIdentity.IdentityException.class)
|
||||||
|
.hasMessageContaining("must be the bindHeight of a v3 epoch");
|
||||||
|
assertThatThrownBy(() -> AerePqIdentity.install(new AerePqIdentity.Config(2000, epochs, 1000)))
|
||||||
|
.isInstanceOf(AerePqIdentity.IdentityException.class);
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs, 2000));
|
||||||
|
assertThat(AerePqIdentity.installed().orElseThrow().keysFrom()).isEqualTo(2000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void belowHkNothingComesFromV3AndStage3aNeverDoes() {
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs, 2000));
|
||||||
|
assertThat(AerePqIdentity.keysFromV3(1999)).isFalse();
|
||||||
|
assertThat(AerePqIdentity.keysFromV3(2000)).isTrue();
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs));
|
||||||
|
assertThat(AerePqIdentity.keysFromV3(Long.MAX_VALUE - 1)).isFalse();
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
assertThat(AerePqIdentity.keysFromV3(5000)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theKeyIsThatIndexOfTheEpochInForceAndTheRotationTakesOverAtItsEpoch() {
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs, 1000));
|
||||||
|
final byte[] fa1 = AerePqIdentity.v3KeyAt(1999, 1, "falcon-512").orElseThrow();
|
||||||
|
final byte[] fb1 = AerePqIdentity.v3KeyAt(2000, 1, "falcon-512").orElseThrow();
|
||||||
|
// the registry form: 896 bytes, the 0x09 header dropped
|
||||||
|
assertThat(fa1).hasSize(896);
|
||||||
|
assertThat(fa1).isEqualTo(Arrays.copyOfRange(key(a, 1, "falcon-512"), 1, 897));
|
||||||
|
assertThat(fb1).isEqualTo(Arrays.copyOfRange(key(b, 1, "falcon-512"), 1, 897));
|
||||||
|
assertThat(fb1).isNotEqualTo(fa1);
|
||||||
|
final byte[] sa1 = AerePqIdentity.v3KeyAt(1999, 1, "slh-dsa-sha2-128s").orElseThrow();
|
||||||
|
assertThat(sa1).hasSize(32).isEqualTo(key(a, 1, "slh-dsa-sha2-128s"));
|
||||||
|
assertThat(AerePqIdentity.v3KeyAt(2000, 1, "slh-dsa-sha2-128s").orElseThrow())
|
||||||
|
.isEqualTo(key(b, 1, "slh-dsa-sha2-128s"))
|
||||||
|
.isNotEqualTo(sa1);
|
||||||
|
// indices belong to the epoch: at 2000 index 0 is the validator that was index 2 in epoch A
|
||||||
|
assertThat(AerePqIdentity.v3KeyAt(2000, 0, "falcon-512").orElseThrow())
|
||||||
|
.isEqualTo(Arrays.copyOfRange(key(a, 2, "falcon-512"), 1, 897));
|
||||||
|
assertThat(AerePqIdentity.v3IdAt(2000, 0)).contains(id(a, 2));
|
||||||
|
assertThat(AerePqIdentity.v3IdAt(2000, 2)).contains(id(b, 2));
|
||||||
|
// an index the epoch does not have: NO key (fail closed), never a fall-back
|
||||||
|
assertThat(AerePqIdentity.v3KeyAt(2000, 3, "falcon-512")).isEmpty();
|
||||||
|
assertThat(AerePqIdentity.v3KeyAt(2000, -1, "falcon-512")).isEmpty();
|
||||||
|
assertThat(AerePqIdentity.v3IdAt(2000, 3)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theContinuityGuardAcceptsTheSameKeysAndRefusesAnyOther() {
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs, 1000));
|
||||||
|
// the existing registries give exactly the keys of epoch A: accepted
|
||||||
|
AerePqIdentity.verifyKeyContinuity(
|
||||||
|
(i, s) -> AerePqIdentity.v3KeyAt(1000, i, s).map(byte[]::clone));
|
||||||
|
// one byte of one SLH-DSA key differs: refused, naming the row and the scheme
|
||||||
|
assertThatThrownBy(
|
||||||
|
() ->
|
||||||
|
AerePqIdentity.verifyKeyContinuity(
|
||||||
|
(i, s) -> {
|
||||||
|
final byte[] k = AerePqIdentity.v3KeyAt(1000, i, s).orElseThrow().clone();
|
||||||
|
if (i == 2 && s.equals("slh-dsa-sha2-128s")) {
|
||||||
|
k[5] ^= 1;
|
||||||
|
}
|
||||||
|
return Optional.of(k);
|
||||||
|
}))
|
||||||
|
.isInstanceOf(AerePqIdentity.IdentityException.class)
|
||||||
|
.hasMessageContaining("row 2 slh-dsa-sha2-128s differs");
|
||||||
|
// a key the existing registries cannot produce: refused
|
||||||
|
assertThatThrownBy(
|
||||||
|
() ->
|
||||||
|
AerePqIdentity.verifyKeyContinuity(
|
||||||
|
(i, s) -> i == 0 ? Optional.empty() : AerePqIdentity.v3KeyAt(1000, i, s)))
|
||||||
|
.isInstanceOf(AerePqIdentity.IdentityException.class)
|
||||||
|
.hasMessageContaining("row 0");
|
||||||
|
// no H_k configured: the guard is inert even with a function that has no keys at all
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs));
|
||||||
|
AerePqIdentity.verifyKeyContinuity((i, s) -> Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theFalconSealSupportDoorsTakeTheirKeyAndIdFromV3FromHk() {
|
||||||
|
// the WIRING, not only the helper: FalconSealSupport.keyAt (behind every Falcon verification) and the index -> identity doors
|
||||||
|
// must answer from the v3 epoch in force from H_k. This node has no registry schedule at all, so any answer from H_k on can
|
||||||
|
// only come from v3; below H_k the doors fall back to the (absent) registries and answer nothing.
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs, 2000));
|
||||||
|
final FalconSealSupport fs = FalconSealSupport.instance();
|
||||||
|
assertThat(fs.publicKeyBytesAtHistoric(2000, 1))
|
||||||
|
.contains(Arrays.copyOfRange(key(b, 1, "falcon-512"), 1, 897));
|
||||||
|
assertThat(fs.publicKeyBytesAtHistoric(2000, 0))
|
||||||
|
.contains(Arrays.copyOfRange(key(a, 2, "falcon-512"), 1, 897));
|
||||||
|
assertThat(fs.publicKeyBytesAtHistoric(2000, 3)).isEmpty();
|
||||||
|
assertThat(fs.addressForIndexAtHistoric(2000, 2)).isEqualTo(id(b, 2));
|
||||||
|
assertThat(fs.addressForIndexAtOwnHead(2000, 0)).isEqualTo(id(a, 2));
|
||||||
|
assertThat(fs.addressForIndexAtHistoric(2000, 3)).isNull();
|
||||||
|
// below H_k: not from v3 (a v3 answer here would be the rotation taking effect early)
|
||||||
|
assertThat(fs.publicKeyBytesAtHistoric(1999, 1))
|
||||||
|
.isNotEqualTo(Optional.of(Arrays.copyOfRange(key(a, 1, "falcon-512"), 1, 897)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anAuthorKeepsItsIdAcrossAnEpochWithoutLinksAndARemovedIdAuthorsNothing() {
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs, 1000));
|
||||||
|
// epoch B carries no ecdsa links; the id of A's index 1 is still registered, so its address keeps it
|
||||||
|
assertThat(b.rows().get(1).ecdsaAddress()).isNull();
|
||||||
|
assertThat(AerePqIdentity.author(ecdsa(a, 1), 1999)).isEqualTo(id(a, 1));
|
||||||
|
assertThat(AerePqIdentity.author(ecdsa(a, 1), 2000)).isEqualTo(id(a, 1));
|
||||||
|
assertThat(AerePqIdentity.author(ecdsa(a, 2), 2500)).isEqualTo(id(a, 2));
|
||||||
|
// A's index 0 left in B: its address is itself again, which is not in the set
|
||||||
|
assertThat(AerePqIdentity.author(ecdsa(a, 0), 1999)).isEqualTo(id(a, 0));
|
||||||
|
assertThat(AerePqIdentity.author(ecdsa(a, 0), 2000)).isEqualTo(ecdsa(a, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3b-ii: probes of the LOCAL keys by height - a node signs, from H_k, with the key of its own row in the v3
|
||||||
|
* epoch in force and at that row's index - on two epochs built here with PROBA keys this test holds (the shared vectors carry no
|
||||||
|
* private keys): epoch A bound at 1000 (v0, v1, v2 at indices 0, 1, 2, the switch epoch) and epoch B bound at 2000 (v2 MOVED to
|
||||||
|
* index 0, v1 ROTATED both keys under its id at index 1 with the rotation signed by its old keys, v0 left, a new v3 at index 2).
|
||||||
|
*
|
||||||
|
* What must hold, each with its negative: a node finds its row by its key and the index moves with the epoch (a key that no row
|
||||||
|
* of the epoch carries finds nothing); the hybrid producer of a rotating node signs with its OLD key up to the rotation epoch and
|
||||||
|
* with its NEXT key from it, at the right index, each seal verifying under exactly one of the two keys; a moved node signs at its
|
||||||
|
* new index; and a node that lacks its rotated key produces nothing from the rotation epoch (the fact the startup guard refuses on).
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.function.LongToIntFunction;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class AereLocalKeysV3Test {
|
||||||
|
|
||||||
|
private static final List<String> SCHEMES = List.of("falcon-512", "slh-dsa-sha2-128s");
|
||||||
|
private static final String SLH = "slh-dsa-sha2-128s";
|
||||||
|
private PqRegistryV3Test.Proba v0;
|
||||||
|
private PqRegistryV3Test.Proba v1;
|
||||||
|
private PqRegistryV3Test.Proba v2;
|
||||||
|
private PqRegistryV3Test.Proba v1b;
|
||||||
|
private PqRegistryV3Test.Proba v3;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void build() {
|
||||||
|
v0 = PqRegistryV3Test.proba();
|
||||||
|
v1 = PqRegistryV3Test.proba();
|
||||||
|
v2 = PqRegistryV3Test.proba();
|
||||||
|
v1b = PqRegistryV3Test.proba();
|
||||||
|
v3 = PqRegistryV3Test.proba();
|
||||||
|
final var d0 = new PqRegistryV3Test.Draft(PqRegistryV3.idOf(v0.falconStd()), PqRegistryV3Test.both(v0), v0);
|
||||||
|
final var d1 = new PqRegistryV3Test.Draft(PqRegistryV3.idOf(v1.falconStd()), PqRegistryV3Test.both(v1), v1);
|
||||||
|
final var d2 = new PqRegistryV3Test.Draft(PqRegistryV3.idOf(v2.falconStd()), PqRegistryV3Test.both(v2), v2);
|
||||||
|
final ObjectNode ma =
|
||||||
|
PqRegistryV3Test.manifest(
|
||||||
|
1000,
|
||||||
|
List.of(
|
||||||
|
PqRegistryV3Test.rowJson(1000, 3, 0, d0, true, true),
|
||||||
|
PqRegistryV3Test.rowJson(1000, 3, 1, d1, true, false),
|
||||||
|
PqRegistryV3Test.rowJson(1000, 3, 2, d2, true, false)));
|
||||||
|
final PqRegistryV3.Registry a = PqRegistryV3.load(ma, PqRegistryV3Test.CHAIN, SCHEMES, null, true);
|
||||||
|
final var d1b = new PqRegistryV3Test.Draft(d1.id(), PqRegistryV3Test.both(v1b), v1);
|
||||||
|
final var d3 = new PqRegistryV3Test.Draft(PqRegistryV3.idOf(v3.falconStd()), PqRegistryV3Test.both(v3), v3);
|
||||||
|
final ObjectNode r1b = PqRegistryV3Test.rowJson(2000, 3, 1, d1b, false, false);
|
||||||
|
PqRegistryV3Test.rotate(r1b, 2000, 3, 1, List.of(v1.falcon(), v1.slh()));
|
||||||
|
final ObjectNode mb =
|
||||||
|
PqRegistryV3Test.manifest(
|
||||||
|
2000,
|
||||||
|
List.of(
|
||||||
|
PqRegistryV3Test.rowJson(2000, 3, 0, d2, false, false),
|
||||||
|
r1b,
|
||||||
|
PqRegistryV3Test.rowJson(2000, 3, 2, d3, false, false)));
|
||||||
|
final PqRegistryV3.Registry b = PqRegistryV3.load(mb, PqRegistryV3Test.CHAIN, SCHEMES, a, false);
|
||||||
|
final TreeMap<Long, PqRegistryV3.Registry> epochs = new TreeMap<>();
|
||||||
|
epochs.put(1000L, a);
|
||||||
|
epochs.put(2000L, b);
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(1000, epochs, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void disarm() {
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] falcon(final PqRegistryV3Test.Proba p) {
|
||||||
|
return p.falcon().publicRegistryForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The local index a node holding these Falcon keys signs as at a height (what FalconSealSupport.localAt resolves). */
|
||||||
|
private static LongToIntFunction indexOf(final PqRegistryV3Test.Proba... keys) {
|
||||||
|
return h -> {
|
||||||
|
for (final PqRegistryV3Test.Proba k : keys) {
|
||||||
|
final Optional<Integer> i = AerePqIdentity.v3IndexOfKey(h, "falcon-512", falcon(k));
|
||||||
|
if (i.isPresent()) {
|
||||||
|
return i.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HybridSealProducer producer(
|
||||||
|
final PqRegistryV3Test.Proba base, final List<PqRegistryV3Test.Proba> next, final LongToIntFunction idx) {
|
||||||
|
return new HybridSealProducer(
|
||||||
|
0L,
|
||||||
|
PqSchemeSchedule.parse("0:falcon-512+" + SLH),
|
||||||
|
-1,
|
||||||
|
Map.of(SLH, base.slh().privateKey()),
|
||||||
|
0L,
|
||||||
|
next.isEmpty() ? Map.of() : Map.of(SLH, next.stream().map(p -> p.slh().privateKey()).toList()),
|
||||||
|
idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean verifies(final SchemeSeal s, final PqRegistryV3Test.Proba key, final Bytes msg) {
|
||||||
|
return SealSchemes.SLH_DSA_128S.verifyRaw(
|
||||||
|
key.slh().publicRegistryForm(), msg.toArrayUnsafe(), s.getSignature().toArrayUnsafe());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aNodeFindsItsRowByItsKeyAndTheIndexMovesWithTheEpoch() {
|
||||||
|
assertThat(AerePqIdentity.v3IndexOfKey(1999, "falcon-512", falcon(v2))).contains(2);
|
||||||
|
assertThat(AerePqIdentity.v3IndexOfKey(2000, "falcon-512", falcon(v2))).contains(0);
|
||||||
|
assertThat(AerePqIdentity.v3IndexOfKey(1999, "falcon-512", falcon(v1))).contains(1);
|
||||||
|
assertThat(AerePqIdentity.v3IndexOfKey(2000, "falcon-512", falcon(v1))).isEmpty();
|
||||||
|
assertThat(AerePqIdentity.v3IndexOfKey(2000, "falcon-512", falcon(v1b))).contains(1);
|
||||||
|
assertThat(AerePqIdentity.v3IndexOfKey(1999, "falcon-512", falcon(v1b))).isEmpty();
|
||||||
|
assertThat(AerePqIdentity.v3IndexOfKey(2000, "falcon-512", falcon(v0))).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aRotatingNodeSignsWithItsOldKeyUpToTheRotationEpochAndItsNextKeyFromIt() {
|
||||||
|
final HybridSealProducer p = producer(v1, List.of(v1b), indexOf(v1, v1b));
|
||||||
|
final Bytes msg = Bytes.wrap("AERE-TEST-COMMIT".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
final List<SchemeSeal> before = p.sealsFor(1999, msg);
|
||||||
|
assertThat(before).hasSize(1);
|
||||||
|
assertThat(before.get(0).getValidatorIndex()).isEqualTo(1);
|
||||||
|
assertThat(verifies(before.get(0), v1, msg)).isTrue();
|
||||||
|
assertThat(verifies(before.get(0), v1b, msg)).isFalse();
|
||||||
|
final List<SchemeSeal> after = p.sealsFor(2000, msg);
|
||||||
|
assertThat(after).hasSize(1);
|
||||||
|
assertThat(after.get(0).getValidatorIndex()).isEqualTo(1);
|
||||||
|
assertThat(verifies(after.get(0), v1b, msg)).isTrue();
|
||||||
|
assertThat(verifies(after.get(0), v1, msg)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aMovedNodeSignsAtItsNewIndex() {
|
||||||
|
final HybridSealProducer p = producer(v2, List.of(), indexOf(v2));
|
||||||
|
final Bytes msg = Bytes.wrap("AERE-TEST-COMMIT".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
assertThat(p.sealsFor(1999, msg).get(0).getValidatorIndex()).isEqualTo(2);
|
||||||
|
final List<SchemeSeal> after = p.sealsFor(2000, msg);
|
||||||
|
assertThat(after.get(0).getValidatorIndex()).isEqualTo(0);
|
||||||
|
assertThat(verifies(after.get(0), v2, msg)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aNodeWithoutItsRotatedKeyProducesNothingFromTheRotationEpoch() {
|
||||||
|
final HybridSealProducer p = producer(v1, List.of(), indexOf(v1, v1b));
|
||||||
|
final Bytes msg = Bytes.wrap("AERE-TEST-COMMIT".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
assertThat(p.sealsFor(1999, msg)).hasSize(1);
|
||||||
|
// its Falcon index is resolvable only through v1b, which this node does not hold: nothing, and the guard's basis is empty
|
||||||
|
final HybridSealProducer onlyOld = producer(v1, List.of(), indexOf(v1));
|
||||||
|
assertThat(onlyOld.sealsFor(2000, msg)).isEmpty();
|
||||||
|
assertThat(onlyOld.v3KeyAt(SealSchemes.SLH_DSA_128S, 2000)).isEmpty();
|
||||||
|
// holding the rotated Falcon key but not the rotated SLH-DSA key: the row's SLH key has no local match either
|
||||||
|
assertThat(p.v3KeyAt(SealSchemes.SLH_DSA_128S, 2000)).isEmpty();
|
||||||
|
assertThat(p.sealsFor(2000, msg)).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3: probes of the identity translation (AerePqIdentity), on the registry v3
|
||||||
|
* conformance vector ok-epoch-a (three rows, each with its ecdsa link, row 0 with a transport).
|
||||||
|
*
|
||||||
|
* What must hold, each with its negative: disarmed, every function is the identity (the build that
|
||||||
|
* carries the class and is never configured is byte for byte the old one); armed, an address becomes
|
||||||
|
* its id exactly from H_id and not one block earlier; an address without a row stays itself (so its
|
||||||
|
* message is simply not in the set); the set after H_id - 1 becomes the sorted ids and a member
|
||||||
|
* without a row REFUSES instead of being dropped; the local node is recognised in both forms and only
|
||||||
|
* its own; ids reach peers through their transport, and an id with no transport means "every peer".
|
||||||
|
*/
|
||||||
|
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.datatypes.Address;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
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.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class AerePqIdentityTest {
|
||||||
|
|
||||||
|
private static final long H_ID = 1500;
|
||||||
|
private PqRegistryV3.Registry epoch;
|
||||||
|
private final List<Address> addr = new ArrayList<>();
|
||||||
|
private final List<Address> id = new ArrayList<>();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void armOnTheVector() 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);
|
||||||
|
}
|
||||||
|
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()) {
|
||||||
|
addr.add(Address.wrap(Bytes.wrap(r.ecdsaAddress())));
|
||||||
|
id.add(Address.wrap(Bytes.wrap(r.id())));
|
||||||
|
}
|
||||||
|
final TreeMap<Long, PqRegistryV3.Registry> epochs = new TreeMap<>();
|
||||||
|
epochs.put(epoch.bindHeight(), epoch);
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
assertThat(epoch.bindHeight()).isLessThanOrEqualTo(H_ID);
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(H_ID, epochs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void disarm() {
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void disarmedEverythingIsTheIdentity() {
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
assertThat(AerePqIdentity.author(addr.get(0), H_ID + 10)).isEqualTo(addr.get(0));
|
||||||
|
final Collection<Address> set = new java.util.TreeSet<>(addr);
|
||||||
|
assertThat(AerePqIdentity.translateSetAfter(set, H_ID - 1)).isSameAs(set);
|
||||||
|
assertThat(AerePqIdentity.isLocal(id.get(0), addr.get(0))).isFalse();
|
||||||
|
assertThat(AerePqIdentity.transportTargets(addr)).contains(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anAddressBecomesItsIdExactlyFromHid() {
|
||||||
|
assertThat(AerePqIdentity.author(addr.get(1), H_ID - 1)).isEqualTo(addr.get(1));
|
||||||
|
assertThat(AerePqIdentity.author(addr.get(1), H_ID)).isEqualTo(id.get(1));
|
||||||
|
assertThat(AerePqIdentity.author(addr.get(2), H_ID + 1000)).isEqualTo(id.get(2));
|
||||||
|
// an address the registry does not link stays itself: its message is then not in the set
|
||||||
|
final Address stranger = Address.fromHexString("0x00000000000000000000000000000000000000aa");
|
||||||
|
assertThat(AerePqIdentity.author(stranger, H_ID + 5)).isEqualTo(stranger);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theSetAfterHidMinusOneBecomesTheSortedIdsAndNowhereElse() {
|
||||||
|
final Collection<Address> set = new java.util.TreeSet<>(addr);
|
||||||
|
assertThat(AerePqIdentity.translateSetAfter(set, H_ID - 2)).isSameAs(set);
|
||||||
|
assertThat(AerePqIdentity.translateSetAfter(set, H_ID)).isSameAs(set);
|
||||||
|
final Collection<Address> t = AerePqIdentity.translateSetAfter(set, H_ID - 1);
|
||||||
|
assertThat(t).containsExactlyElementsOf(new java.util.TreeSet<>(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aMemberWithoutARowRefusesInsteadOfBeingDropped() {
|
||||||
|
final Collection<Address> set = new java.util.TreeSet<>(addr);
|
||||||
|
set.add(Address.fromHexString("0x00000000000000000000000000000000000000bb"));
|
||||||
|
assertThatThrownBy(() -> AerePqIdentity.translateSetAfter(set, H_ID - 1))
|
||||||
|
.isInstanceOf(AerePqIdentity.IdentityException.class)
|
||||||
|
.hasMessageContaining("has no row");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theLocalNodeIsRecognisedInBothFormsAndOnlyItsOwn() {
|
||||||
|
assertThat(AerePqIdentity.isLocal(addr.get(0), addr.get(0))).isTrue();
|
||||||
|
assertThat(AerePqIdentity.isLocal(id.get(0), addr.get(0))).isTrue();
|
||||||
|
assertThat(AerePqIdentity.isLocal(id.get(1), addr.get(0))).isFalse();
|
||||||
|
assertThat(AerePqIdentity.local(addr.get(0), H_ID - 1)).isEqualTo(addr.get(0));
|
||||||
|
assertThat(AerePqIdentity.local(addr.get(0), H_ID)).isEqualTo(id.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void idsReachPeersThroughTheirTransportOrEveryPeer() {
|
||||||
|
final byte[] t0 = epoch.rows().get(0).transport();
|
||||||
|
assertThat(t0).isNotNull();
|
||||||
|
assertThat(AerePqIdentity.transportTargets(List.of(id.get(0))))
|
||||||
|
.contains(List.of(Address.wrap(Bytes.wrap(t0))));
|
||||||
|
assertThat(epoch.rows().get(1).transport()).isNull();
|
||||||
|
assertThat(AerePqIdentity.transportTargets(List.of(id.get(0), id.get(1)))).isEqualTo(Optional.empty());
|
||||||
|
// below H_id the recipients are addresses and go out as before
|
||||||
|
assertThat(AerePqIdentity.transportTargets(addr)).contains(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anIdentityHeightWithNoEpochAtOrBelowItRefusesToArm() {
|
||||||
|
final TreeMap<Long, PqRegistryV3.Registry> epochs = new TreeMap<>();
|
||||||
|
epochs.put(epoch.bindHeight(), epoch);
|
||||||
|
assertThatThrownBy(
|
||||||
|
() -> AerePqIdentity.install(new AerePqIdentity.Config(epoch.bindHeight() - 1, epochs)))
|
||||||
|
.isInstanceOf(AerePqIdentity.IdentityException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -92,6 +92,35 @@ class HybridSealProducerTest {
|
|||||||
assertThat(seals.get(0).getValidatorIndex()).isEqualTo(INDEX);
|
assertThat(seals.get(0).getValidatorIndex()).isEqualTo(INDEX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------- intervale per schema (AIP-22 treapta 2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With SLH-DSA on the 128 grid and the anchor grids counted from H0 = 64, the producer signs the
|
||||||
|
* SLH-DSA seal only at the parent of an anchor on that grid; at the parent of a Falcon-only
|
||||||
|
* anchor (32 grid) it emits nothing, although the key is there and the gate is open. The two
|
||||||
|
* heights differ in nothing but the grid, which is the only thing this proof is about.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void theSlhDsaSealIsProducedOnlyAtParentsOfAnchorsOnItsOwnGrid() {
|
||||||
|
final long h0 = 64L;
|
||||||
|
final PqSchemeSchedule cuIntervale =
|
||||||
|
PqSchemeSchedule.parse(
|
||||||
|
"0:" + SealSchemes.FALCON_512.id() + "/32+" + SealSchemes.SLH_DSA_128S.id() + "/128");
|
||||||
|
final HybridSealProducer p =
|
||||||
|
new HybridSealProducer(
|
||||||
|
H_ATASARE, cuIntervale, INDEX, Map.of(SealSchemes.SLH_DSA_128S.id(), slh.privateKey()), h0);
|
||||||
|
final long ancoraHibrida = h0 + 128L * 10; // 1344: pe grila 128
|
||||||
|
final long ancoraDoarFalcon = ancoraHibrida + 32L; // 1376: pe grila 32, nu pe 128
|
||||||
|
assertThat(p.sealsFor(ancoraHibrida - 1, MESSAGE)).hasSize(1);
|
||||||
|
assertThat(p.sealsFor(ancoraDoarFalcon - 1, MESSAGE)).isEmpty();
|
||||||
|
// the negative control of the grid origin: the same heights read against another origin flip
|
||||||
|
final HybridSealProducer altaOrigine =
|
||||||
|
new HybridSealProducer(
|
||||||
|
H_ATASARE, cuIntervale, INDEX, Map.of(SealSchemes.SLH_DSA_128S.id(), slh.privateKey()), h0 + 32);
|
||||||
|
assertThat(altaOrigine.sealsFor(ancoraHibrida - 1, MESSAGE)).isEmpty();
|
||||||
|
assertThat(altaOrigine.sealsFor(ancoraDoarFalcon - 1, MESSAGE)).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- jumatatea de certificat
|
// ---------------------------------------------------------------- jumatatea de certificat
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -0,0 +1,509 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3: probes of the v3 registry, and the writer of its conformance vectors.
|
||||||
|
*
|
||||||
|
* TWO MODES. With the environment variable AERE_REG3_VECTORS_OUT set to a directory, the vector
|
||||||
|
* writer generates PROBA keys (its helpers are package-visible since 2026-09-23: AereLocalKeysV3Test builds its own epochs with
|
||||||
|
* private keys it holds) (Falcon-512, SLH-DSA-SHA2-128s, secp256k1), builds two epochs and one
|
||||||
|
* negative vector per refusal code, and writes them as JSON; each negative differs from a valid
|
||||||
|
* manifest in ONE field (the lesson of AIP-20: a negative that differs in two places measures which
|
||||||
|
* check the validator reaches first, not the one it names), and where a field feeds a signed
|
||||||
|
* message the signatures are re-made over the changed field so that only the named rule can fail.
|
||||||
|
* Without the variable, every test reads the checked-in vectors from test resources
|
||||||
|
* (aere/registry-v3/) and requires the outcome each vector names: "ok" with the stated registry hash
|
||||||
|
* and row hashes, or the exact refusal code. The same files are read by the Nethermind port and by
|
||||||
|
* the public JS tool, which is what makes them conformance vectors and not just fixtures.
|
||||||
|
*/
|
||||||
|
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.crypto.KeyPair;
|
||||||
|
import org.hyperledger.besu.crypto.SecureRandomProvider;
|
||||||
|
import org.hyperledger.besu.crypto.SignatureAlgorithm;
|
||||||
|
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
|
||||||
|
import org.hyperledger.besu.ethereum.core.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||||
|
|
||||||
|
public class PqRegistryV3Test {
|
||||||
|
|
||||||
|
private static final ObjectMapper M =
|
||||||
|
new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||||
|
private static final String RES = "/aere/registry-v3/";
|
||||||
|
private static final List<String> VECTORS =
|
||||||
|
List.of(
|
||||||
|
"ok-epoch-a", "ok-epoch-b-rotation", "n01-format", "n02-chain", "n03-extra-field",
|
||||||
|
"n05-unknown-scheme", "n06-required-scheme", "n07-falcon-header", "n08-pop",
|
||||||
|
"n09-id-derivation", "n10-duplicate-id", "n11-duplicate-key", "n12-rotation-one-key",
|
||||||
|
"n12b-rotation-wrong-key", "n13-rotation-unchanged", "n14-ecdsa-wrong-key",
|
||||||
|
"n14b-ecdsa-missing", "n15-transport", "n16-declared-hash");
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------
|
||||||
|
// The conformance vectors
|
||||||
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyVectorHasTheOutcomeItNames() throws IOException {
|
||||||
|
int ok = 0;
|
||||||
|
int refused = 0;
|
||||||
|
for (final String name : VECTORS) {
|
||||||
|
final JsonNode v = vector(name);
|
||||||
|
final PqRegistryV3.Registry prev =
|
||||||
|
v.hasNonNull("previous")
|
||||||
|
? loadVector(vector(v.get("previous").asText()), null)
|
||||||
|
: null;
|
||||||
|
final String expect = v.get("expect").asText();
|
||||||
|
if ("ok".equals(expect)) {
|
||||||
|
final PqRegistryV3.Registry r = loadVector(v, prev);
|
||||||
|
assertThat(r.hash().toHexString()).as(name).isEqualTo(v.get("registryHash").asText());
|
||||||
|
final ArrayNode rh = (ArrayNode) v.get("rowHashes");
|
||||||
|
assertThat(r.rowHashes()).as(name).hasSize(rh.size());
|
||||||
|
for (int i = 0; i < rh.size(); i++) {
|
||||||
|
assertThat(r.rowHashes().get(i).toHexString()).as(name + " row " + i)
|
||||||
|
.isEqualTo(rh.get(i).asText());
|
||||||
|
}
|
||||||
|
ok++;
|
||||||
|
} else {
|
||||||
|
final PqRegistryV3.Registry prevF = prev;
|
||||||
|
assertThatThrownBy(() -> loadVector(v, prevF))
|
||||||
|
.as(name)
|
||||||
|
.isInstanceOf(PqRegistryV3.RegistryV3Exception.class)
|
||||||
|
.hasMessageStartingWith(expect + ":");
|
||||||
|
refused++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(ok).isEqualTo(2);
|
||||||
|
assertThat(refused).isEqualTo(VECTORS.size() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void merkleProofsVerifyForEveryRowAndNotOneRowPastTheEnd() throws IOException {
|
||||||
|
final JsonNode v = vector("ok-epoch-a");
|
||||||
|
final PqRegistryV3.Registry r = loadVector(v, null);
|
||||||
|
for (int i = 0; i < r.count(); i++) {
|
||||||
|
final List<Bytes32> p = PqRegistryV3.merkleProof(r.rowHashes(), i);
|
||||||
|
assertThat(PqRegistryV3.verifyMerkleProof(r.rowHashes().get(i), i, r.count(), p, r.root()))
|
||||||
|
.as("row " + i)
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
// three rows: the last leaf is paired with itself, so its path also "proves" index 3 unless the
|
||||||
|
// count bound refuses it - that refusal is the whole point of the bound
|
||||||
|
final int last = r.count() - 1;
|
||||||
|
final List<Bytes32> p = PqRegistryV3.merkleProof(r.rowHashes(), last);
|
||||||
|
assertThat(r.count() % 2).isEqualTo(1);
|
||||||
|
assertThat(
|
||||||
|
PqRegistryV3.verifyMerkleProof(r.rowHashes().get(last), last + 1, r.count(), p, r.root()))
|
||||||
|
.isFalse();
|
||||||
|
// and a leaf that is not a row never verifies
|
||||||
|
assertThat(
|
||||||
|
PqRegistryV3.verifyMerkleProof(
|
||||||
|
Bytes32.fromHexStringLenient("0x01"), 0, r.count(),
|
||||||
|
PqRegistryV3.merkleProof(r.rowHashes(), 0), r.root()))
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theIdIsTheAip20SenderDerivationOfTheFalconKey() throws IOException {
|
||||||
|
final JsonNode v = vector("ok-epoch-a");
|
||||||
|
final PqRegistryV3.Registry r = loadVector(v, null);
|
||||||
|
for (final PqRegistryV3.Row row : r.rows()) {
|
||||||
|
final byte[] pk = row.key("falcon-512").orElseThrow().publicKey();
|
||||||
|
assertThat(pk).hasSize(897);
|
||||||
|
assertThat(pk[0]).isEqualTo((byte) 0x09);
|
||||||
|
assertThat(PqRegistryV3.idOf(pk)).isEqualTo(row.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theRegistryHashCommitsToChainHeightAndCount() throws IOException {
|
||||||
|
final JsonNode v = vector("ok-epoch-a");
|
||||||
|
final PqRegistryV3.Registry r = loadVector(v, null);
|
||||||
|
final Bytes32 h = PqRegistryV3.registryHash(r.chainId(), r.bindHeight(), r.count(), r.root());
|
||||||
|
assertThat(h).isEqualTo(r.hash());
|
||||||
|
assertThat(PqRegistryV3.registryHash(r.chainId() + 1, r.bindHeight(), r.count(), r.root()))
|
||||||
|
.isNotEqualTo(h);
|
||||||
|
assertThat(PqRegistryV3.registryHash(r.chainId(), r.bindHeight() + 1, r.count(), r.root()))
|
||||||
|
.isNotEqualTo(h);
|
||||||
|
assertThat(PqRegistryV3.registryHash(r.chainId(), r.bindHeight(), r.count() + 1, r.root()))
|
||||||
|
.isNotEqualTo(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void thePinnedHashIsCompared() throws IOException {
|
||||||
|
final JsonNode v = vector("ok-epoch-a");
|
||||||
|
final Bytes32 good = Bytes32.fromHexString(v.get("registryHash").asText());
|
||||||
|
final PqRegistryV3.Registry r =
|
||||||
|
PqRegistryV3.loadPinned(
|
||||||
|
v.get("manifest"), v.get("chainId").asLong(), required(v), null,
|
||||||
|
v.get("ecdsaRequired").asBoolean(), good);
|
||||||
|
assertThat(r.hash()).isEqualTo(good);
|
||||||
|
assertThatThrownBy(
|
||||||
|
() ->
|
||||||
|
PqRegistryV3.loadPinned(
|
||||||
|
v.get("manifest"), v.get("chainId").asLong(), required(v), null,
|
||||||
|
v.get("ecdsaRequired").asBoolean(), Bytes32.ZERO))
|
||||||
|
.isInstanceOf(PqRegistryV3.RegistryV3Exception.class)
|
||||||
|
.hasMessageStartingWith("AERE-PQC-REG3-16:");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PqRegistryV3.Registry loadVector(final JsonNode v, final PqRegistryV3.Registry prev) {
|
||||||
|
return PqRegistryV3.load(
|
||||||
|
v.get("manifest"), v.get("chainId").asLong(), required(v), prev,
|
||||||
|
v.get("ecdsaRequired").asBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> required(final JsonNode v) {
|
||||||
|
final List<String> out = new ArrayList<>();
|
||||||
|
v.get("requiredSchemes").forEach(n -> out.add(n.asText()));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonNode vector(final String name) throws IOException {
|
||||||
|
try (InputStream in = PqRegistryV3Test.class.getResourceAsStream(RES + name + ".json")) {
|
||||||
|
assertThat(in).as("vector " + name + " is on the test classpath").isNotNull();
|
||||||
|
return M.readTree(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------
|
||||||
|
// The vector writer (runs only with AERE_REG3_VECTORS_OUT)
|
||||||
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static final long CHAIN = 28001;
|
||||||
|
private static final SecureRandom RNG = SecureRandomProvider.createSecureRandom();
|
||||||
|
private static final SignatureAlgorithm EC = SignatureAlgorithmFactory.getInstance();
|
||||||
|
|
||||||
|
/** One validator's PROBA keys. */
|
||||||
|
record Proba(
|
||||||
|
SealScheme.GeneratedPair falcon, SealScheme.GeneratedPair slh, KeyPair ecdsa, byte[] node) {
|
||||||
|
byte[] falconStd() {
|
||||||
|
final byte[] h = falcon.publicRegistryForm();
|
||||||
|
final byte[] s = new byte[897];
|
||||||
|
s[0] = 0x09;
|
||||||
|
System.arraycopy(h, 0, s, 1, 896);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] address() {
|
||||||
|
return Util.publicKeyToAddress(ecdsa.getPublicKey()).getBytes().toArrayUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Proba proba() {
|
||||||
|
final KeyPair node = EC.generateKeyPair();
|
||||||
|
return new Proba(
|
||||||
|
SealSchemes.FALCON_512.generate(RNG),
|
||||||
|
SealSchemes.SLH_DSA_128S.generate(RNG),
|
||||||
|
EC.generateKeyPair(),
|
||||||
|
Util.publicKeyToAddress(node.getPublicKey()).getBytes().toArrayUnsafe());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A row under construction: keys (scheme id to pair) with the id they are filed under. */
|
||||||
|
record Draft(byte[] id, Map<String, SealScheme.GeneratedPair> keys, Proba owner) {}
|
||||||
|
|
||||||
|
static ObjectNode rowJson(
|
||||||
|
final long bind, final int count, final int index, final Draft d, final boolean ecdsa,
|
||||||
|
final boolean transport) {
|
||||||
|
final ObjectNode row = M.createObjectNode();
|
||||||
|
row.put("id", hx(d.id()));
|
||||||
|
final ObjectNode keys = row.putObject("keys");
|
||||||
|
final ObjectNode pop = row.putObject("pop");
|
||||||
|
for (final Map.Entry<String, SealScheme.GeneratedPair> e : d.keys().entrySet()) {
|
||||||
|
final SealScheme s = SealSchemes.byId(e.getKey()).orElseThrow();
|
||||||
|
final byte[] std = standard(s, e.getValue());
|
||||||
|
keys.put(e.getKey(), hx(std));
|
||||||
|
final Bytes32 msg = PqRegistryV3.popMessage(CHAIN, bind, count, index, d.id(), s.wireId(), std);
|
||||||
|
pop.put(e.getKey(), hx(s.sign(e.getValue().privateKey(), msg.toArrayUnsafe()).orElseThrow()));
|
||||||
|
}
|
||||||
|
if (transport) {
|
||||||
|
row.putObject("transport").put("secp256k1Address", hx(d.owner().node()));
|
||||||
|
}
|
||||||
|
if (ecdsa) {
|
||||||
|
claim(row, bind, count, index, d.owner().ecdsa());
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** (Re)computes the ecdsa claim of a row from its current JSON, with the given key. */
|
||||||
|
private static void claim(
|
||||||
|
final ObjectNode row, final long bind, final int count, final int index, final KeyPair kp) {
|
||||||
|
final Bytes32 rh = rowHashOf(row, bind, count, index);
|
||||||
|
final Bytes32 d = PqRegistryV3.claimDigest(CHAIN, rh);
|
||||||
|
final ObjectNode ec = row.putObject("ecdsa");
|
||||||
|
ec.put("addr", hx(Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe()));
|
||||||
|
ec.put("claim", hx(EC.sign(d, kp).encodedBytes().toArrayUnsafe()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The row hash of a row as written in JSON (reusing the loader's own parser via a 1-row load
|
||||||
|
* is impossible here, so the pre-image is rebuilt from the JSON fields). */
|
||||||
|
private static Bytes32 rowHashOf(final ObjectNode row, final long bind, final int count, final int index) {
|
||||||
|
final TreeMap<Integer, PqRegistryV3.Key> keys = new TreeMap<>();
|
||||||
|
final Iterator<Map.Entry<String, JsonNode>> it = row.get("keys").fields();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
final Map.Entry<String, JsonNode> e = it.next();
|
||||||
|
final SealScheme s = SealSchemes.byId(e.getKey()).orElseThrow();
|
||||||
|
keys.put(
|
||||||
|
s.wireId() & 0xff,
|
||||||
|
new PqRegistryV3.Key(
|
||||||
|
s, Bytes.fromHexString(e.getValue().asText()).toArrayUnsafe(),
|
||||||
|
Bytes.fromHexString(row.get("pop").get(e.getKey()).asText()).toArrayUnsafe()));
|
||||||
|
}
|
||||||
|
final byte[] transport =
|
||||||
|
row.has("transport")
|
||||||
|
? Bytes.fromHexString(row.get("transport").get("secp256k1Address").asText()).toArrayUnsafe()
|
||||||
|
: null;
|
||||||
|
final PqRegistryV3.Row r =
|
||||||
|
new PqRegistryV3.Row(
|
||||||
|
index, Bytes.fromHexString(row.get("id").asText()).toArrayUnsafe(), keys, transport,
|
||||||
|
Map.of(), null, null);
|
||||||
|
return PqRegistryV3.rowHash(CHAIN, bind, count, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] standard(final SealScheme s, final SealScheme.GeneratedPair p) {
|
||||||
|
if (s == SealSchemes.FALCON_512) {
|
||||||
|
final byte[] h = p.publicRegistryForm();
|
||||||
|
final byte[] std = new byte[897];
|
||||||
|
std[0] = 0x09;
|
||||||
|
System.arraycopy(h, 0, std, 1, 896);
|
||||||
|
return std;
|
||||||
|
}
|
||||||
|
return p.publicRegistryForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ObjectNode manifest(final long bind, final List<ObjectNode> rows) {
|
||||||
|
final ObjectNode m = M.createObjectNode();
|
||||||
|
m.put("formatVersion", 3);
|
||||||
|
m.put("chainId", CHAIN);
|
||||||
|
m.put("bindHeight", bind);
|
||||||
|
m.put("count", rows.size());
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
m.set(Integer.toString(i), rows.get(i));
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectNode wrap(
|
||||||
|
final String description, final ObjectNode manifest, final boolean ecdsaRequired,
|
||||||
|
final String previous, final String expect) {
|
||||||
|
final ObjectNode v = M.createObjectNode();
|
||||||
|
v.put("description", description);
|
||||||
|
v.put("chainId", CHAIN);
|
||||||
|
final ArrayNode req = v.putArray("requiredSchemes");
|
||||||
|
req.add("falcon-512");
|
||||||
|
req.add("slh-dsa-sha2-128s");
|
||||||
|
v.put("ecdsaRequired", ecdsaRequired);
|
||||||
|
if (previous == null) {
|
||||||
|
v.putNull("previous");
|
||||||
|
} else {
|
||||||
|
v.put("previous", previous);
|
||||||
|
}
|
||||||
|
v.put("expect", expect);
|
||||||
|
v.set("manifest", manifest);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SealScheme.GeneratedPair> both(final Proba p) {
|
||||||
|
final Map<String, SealScheme.GeneratedPair> m = new java.util.LinkedHashMap<>();
|
||||||
|
m.put("falcon-512", p.falcon());
|
||||||
|
m.put("slh-dsa-sha2-128s", p.slh());
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@EnabledIfEnvironmentVariable(named = "AERE_REG3_VECTORS_OUT", matches = ".+")
|
||||||
|
public void writeTheConformanceVectors() throws IOException {
|
||||||
|
final Path out = Path.of(System.getenv("AERE_REG3_VECTORS_OUT"));
|
||||||
|
Files.createDirectories(out);
|
||||||
|
final Proba v0 = proba();
|
||||||
|
final Proba v1 = proba();
|
||||||
|
final Proba v2 = proba();
|
||||||
|
final Proba v3 = proba();
|
||||||
|
final long bindA = 1000;
|
||||||
|
final long bindB = 2000;
|
||||||
|
final Draft d0 = new Draft(PqRegistryV3.idOf(v0.falconStd()), both(v0), v0);
|
||||||
|
final Draft d1 = new Draft(PqRegistryV3.idOf(v1.falconStd()), both(v1), v1);
|
||||||
|
final Draft d2 = new Draft(PqRegistryV3.idOf(v2.falconStd()), both(v2), v2);
|
||||||
|
|
||||||
|
// epoch A: three rows, the switch epoch (ecdsa on every row), transport on row 0
|
||||||
|
final List<ObjectNode> a = new ArrayList<>();
|
||||||
|
a.add(rowJson(bindA, 3, 0, d0, true, true));
|
||||||
|
a.add(rowJson(bindA, 3, 1, d1, true, false));
|
||||||
|
a.add(rowJson(bindA, 3, 2, d2, true, false));
|
||||||
|
final ObjectNode ma = manifest(bindA, a);
|
||||||
|
final ObjectNode okA = wrap("epoch A: 3 rows, falcon-512 + slh-dsa-sha2-128s, the switch epoch", ma, true, null, "ok");
|
||||||
|
final PqRegistryV3.Registry ra = PqRegistryV3.load(ma, CHAIN, List.of("falcon-512", "slh-dsa-sha2-128s"), null, true);
|
||||||
|
addHashes(okA, ra);
|
||||||
|
write(out, "ok-epoch-a", okA);
|
||||||
|
|
||||||
|
// epoch B: row 1 ROTATES both keys (keeps its id), row 2 unchanged, a new validator joins, v0 leaves
|
||||||
|
final Proba v1b = proba();
|
||||||
|
final Draft d1b = new Draft(d1.id(), both(v1b), v1);
|
||||||
|
final Draft d3 = new Draft(PqRegistryV3.idOf(v3.falconStd()), both(v3), v3);
|
||||||
|
final List<ObjectNode> b = new ArrayList<>();
|
||||||
|
b.add(rowJson(bindB, 3, 0, d2, false, false));
|
||||||
|
final ObjectNode r1b = rowJson(bindB, 3, 1, d1b, false, false);
|
||||||
|
rotate(r1b, bindB, 3, 1, List.of(v1.falcon(), v1.slh()));
|
||||||
|
b.add(r1b);
|
||||||
|
b.add(rowJson(bindB, 3, 2, d3, false, false));
|
||||||
|
final ObjectNode mb = manifest(bindB, b);
|
||||||
|
final ObjectNode okB = wrap("epoch B after A: index 1 rotates both keys under its id, index 0 unchanged (moved), a new id joins", mb, false, "ok-epoch-a", "ok");
|
||||||
|
final PqRegistryV3.Registry rb = PqRegistryV3.load(mb, CHAIN, List.of("falcon-512", "slh-dsa-sha2-128s"), ra, false);
|
||||||
|
addHashes(okB, rb);
|
||||||
|
write(out, "ok-epoch-b-rotation", okB);
|
||||||
|
|
||||||
|
// negatives on epoch A, one field each
|
||||||
|
ObjectNode m = ma.deepCopy();
|
||||||
|
m.put("formatVersion", 2);
|
||||||
|
write(out, "n01-format", wrap("formatVersion 2", m, true, null, "AERE-PQC-REG3-01"));
|
||||||
|
m = ma.deepCopy();
|
||||||
|
m.put("chainId", 2800);
|
||||||
|
write(out, "n02-chain", wrap("chainId of another chain", m, true, null, "AERE-PQC-REG3-02"));
|
||||||
|
m = ma.deepCopy();
|
||||||
|
m.put("3", M.createObjectNode());
|
||||||
|
write(out, "n03-extra-field", wrap("a row \"3\" beyond count 3", m, true, null, "AERE-PQC-REG3-03"));
|
||||||
|
m = ma.deepCopy();
|
||||||
|
((ObjectNode) m.get("1").get("keys")).put("ml-dsa-65", "0x00");
|
||||||
|
((ObjectNode) m.get("1").get("pop")).put("ml-dsa-65", "0x00");
|
||||||
|
write(out, "n05-unknown-scheme", wrap("a key of a scheme this build does not know", m, true, null, "AERE-PQC-REG3-05"));
|
||||||
|
// n06: row 2 without its SLH-DSA key; pop and claim re-made so only the missing scheme can fail
|
||||||
|
{
|
||||||
|
final Map<String, SealScheme.GeneratedPair> onlyF = new java.util.LinkedHashMap<>();
|
||||||
|
onlyF.put("falcon-512", v2.falcon());
|
||||||
|
final List<ObjectNode> x = new ArrayList<>(a);
|
||||||
|
x.set(2, rowJson(bindA, 3, 2, new Draft(d2.id(), onlyF, v2), true, false));
|
||||||
|
write(out, "n06-required-scheme", wrap("row 2 carries no slh-dsa key", manifest(bindA, x), true, null, "AERE-PQC-REG3-06"));
|
||||||
|
}
|
||||||
|
m = ma.deepCopy();
|
||||||
|
final String fk = m.get("0").get("keys").get("falcon-512").asText();
|
||||||
|
((ObjectNode) m.get("0").get("keys")).put("falcon-512", "0x0a" + fk.substring(4));
|
||||||
|
write(out, "n07-falcon-header", wrap("row 0 falcon key with header 0x0a instead of 0x09", m, true, null, "AERE-PQC-REG3-07"));
|
||||||
|
m = ma.deepCopy();
|
||||||
|
((ObjectNode) m.get("1").get("pop")).put("falcon-512", m.get("0").get("pop").get("falcon-512").asText());
|
||||||
|
write(out, "n08-pop", wrap("row 1 carries row 0's falcon possession proof", m, true, null, "AERE-PQC-REG3-08"));
|
||||||
|
// n09: row 1 filed under an id that is not its derivation; every proof re-made over that id
|
||||||
|
{
|
||||||
|
final byte[] wrongId = new byte[20];
|
||||||
|
RNG.nextBytes(wrongId);
|
||||||
|
final List<ObjectNode> x = new ArrayList<>(a);
|
||||||
|
x.set(1, rowJson(bindA, 3, 1, new Draft(wrongId, both(v1), v1), true, false));
|
||||||
|
write(out, "n09-id-derivation", wrap("row 1 id is not keccak(0x50||0x20||falcon)[12:]", manifest(bindA, x), true, null, "AERE-PQC-REG3-09"));
|
||||||
|
}
|
||||||
|
// n10: row 2 filed under row 1's id; proofs re-made
|
||||||
|
{
|
||||||
|
final List<ObjectNode> x = new ArrayList<>(a);
|
||||||
|
x.set(2, rowJson(bindA, 3, 2, new Draft(d1.id(), both(v2), v2), true, false));
|
||||||
|
write(out, "n10-duplicate-id", wrap("row 2 carries row 1's id", manifest(bindA, x), true, null, "AERE-PQC-REG3-10"));
|
||||||
|
}
|
||||||
|
// n11: row 2 carries row 1's SLH-DSA key (its proof made by that key, over row 2)
|
||||||
|
{
|
||||||
|
final Map<String, SealScheme.GeneratedPair> k = new java.util.LinkedHashMap<>();
|
||||||
|
k.put("falcon-512", v2.falcon());
|
||||||
|
k.put("slh-dsa-sha2-128s", v1.slh());
|
||||||
|
final List<ObjectNode> x = new ArrayList<>(a);
|
||||||
|
x.set(2, rowJson(bindA, 3, 2, new Draft(d2.id(), k, v2), true, false));
|
||||||
|
write(out, "n11-duplicate-key", wrap("row 2 carries row 1's slh-dsa key", manifest(bindA, x), true, null, "AERE-PQC-REG3-11"));
|
||||||
|
}
|
||||||
|
// n15: transport of 19 bytes (claim re-made over the row as written is impossible: the loader
|
||||||
|
// refuses the field before hashing, which is the point)
|
||||||
|
m = ma.deepCopy();
|
||||||
|
((ObjectNode) m.get("0").get("transport")).put("secp256k1Address", "0x" + "11".repeat(19));
|
||||||
|
write(out, "n15-transport", wrap("row 0 transport is 19 bytes", m, true, null, "AERE-PQC-REG3-15"));
|
||||||
|
// n14: row 1's claim made by row 2's classical key; n14b: row 1 without the link in the switch epoch
|
||||||
|
{
|
||||||
|
final ObjectNode x1 = a.get(1).deepCopy();
|
||||||
|
claim(x1, bindA, 3, 1, v2.ecdsa());
|
||||||
|
((ObjectNode) x1.get("ecdsa")).put("addr", hx(v1.address()));
|
||||||
|
final List<ObjectNode> x = new ArrayList<>(a);
|
||||||
|
x.set(1, x1);
|
||||||
|
write(out, "n14-ecdsa-wrong-key", wrap("row 1 claim signed by another validator's key", manifest(bindA, x), true, null, "AERE-PQC-REG3-14"));
|
||||||
|
final ObjectNode y1 = a.get(1).deepCopy();
|
||||||
|
y1.remove("ecdsa");
|
||||||
|
final List<ObjectNode> y = new ArrayList<>(a);
|
||||||
|
y.set(1, y1);
|
||||||
|
write(out, "n14b-ecdsa-missing", wrap("row 1 has no ecdsa link in the switch epoch", manifest(bindA, y), true, null, "AERE-PQC-REG3-14"));
|
||||||
|
}
|
||||||
|
m = ma.deepCopy();
|
||||||
|
m.put("registryHash", "0x" + "00".repeat(32));
|
||||||
|
write(out, "n16-declared-hash", wrap("the file declares a registry hash it does not have", m, true, null, "AERE-PQC-REG3-16"));
|
||||||
|
|
||||||
|
// negatives on epoch B
|
||||||
|
{
|
||||||
|
final ObjectNode x1 = rowJson(bindB, 3, 1, d1b, false, false);
|
||||||
|
rotate(x1, bindB, 3, 1, List.of(v1.falcon()));
|
||||||
|
final List<ObjectNode> x = new ArrayList<>(b);
|
||||||
|
x.set(1, x1);
|
||||||
|
write(out, "n12-rotation-one-key", wrap("index 1 rotates with the old falcon key only (slh missing)", manifest(bindB, x), false, "ok-epoch-a", "AERE-PQC-REG3-12"));
|
||||||
|
final ObjectNode z1 = rowJson(bindB, 3, 1, d1b, false, false);
|
||||||
|
rotate(z1, bindB, 3, 1, List.of(v3.falcon(), v1.slh()));
|
||||||
|
final List<ObjectNode> z = new ArrayList<>(b);
|
||||||
|
z.set(1, z1);
|
||||||
|
write(out, "n12b-rotation-wrong-key", wrap("index 1 rotation's falcon signature is by another validator's key", manifest(bindB, z), false, "ok-epoch-a", "AERE-PQC-REG3-12"));
|
||||||
|
final ObjectNode u0 = rowJson(bindB, 3, 0, d2, false, false);
|
||||||
|
rotate(u0, bindB, 3, 0, List.of(v2.falcon(), v2.slh()));
|
||||||
|
final List<ObjectNode> u = new ArrayList<>(b);
|
||||||
|
u.set(0, u0);
|
||||||
|
write(out, "n13-rotation-unchanged", wrap("index 0 did not change its keys and carries a rotation", manifest(bindB, u), false, "ok-epoch-a", "AERE-PQC-REG3-13"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds rotation signatures over the row's current hash, one per given old key, in scheme order. */
|
||||||
|
static void rotate(
|
||||||
|
final ObjectNode row, final long bind, final int count, final int index,
|
||||||
|
final List<SealScheme.GeneratedPair> oldKeys) {
|
||||||
|
final Bytes32 msg = PqRegistryV3.rotateMessage(rowHashOf(row, bind, count, index));
|
||||||
|
final ObjectNode rot = row.putObject("rotation");
|
||||||
|
final String[] names = {"falcon-512", "slh-dsa-sha2-128s"};
|
||||||
|
for (int i = 0; i < oldKeys.size(); i++) {
|
||||||
|
final SealScheme s = SealSchemes.byId(names[i]).orElseThrow();
|
||||||
|
rot.put(names[i], hx(s.sign(oldKeys.get(i).privateKey(), msg.toArrayUnsafe()).orElseThrow()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addHashes(final ObjectNode v, final PqRegistryV3.Registry r) {
|
||||||
|
v.put("registryHash", r.hash().toHexString());
|
||||||
|
v.put("root", r.root().toHexString());
|
||||||
|
final ArrayNode rh = v.putArray("rowHashes");
|
||||||
|
r.rowHashes().forEach(h -> rh.add(h.toHexString()));
|
||||||
|
final ObjectNode proofs = v.putObject("merkleProofs");
|
||||||
|
for (int i = 0; i < r.count(); i++) {
|
||||||
|
final ArrayNode p = proofs.putArray(Integer.toString(i));
|
||||||
|
PqRegistryV3.merkleProof(r.rowHashes(), i).forEach(h -> p.add(h.toHexString()));
|
||||||
|
}
|
||||||
|
final ArrayNode ids = v.putArray("ids");
|
||||||
|
r.rows().forEach(row -> ids.add(hx(row.id())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void write(final Path dir, final String name, final JsonNode v) throws IOException {
|
||||||
|
Files.writeString(dir.resolve(name + ".json"), M.writeValueAsString(v) + "\n", StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hx(final byte[] b) {
|
||||||
|
final StringBuilder sb = new StringBuilder("0x");
|
||||||
|
for (final byte x : b) {
|
||||||
|
sb.append(String.format(Locale.ROOT, "%02x", x & 0xff));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -116,6 +116,58 @@ class PqSchemeScheduleTest {
|
|||||||
assertThat(orar.firstUnsatisfied(reg, 4)).isPresent();
|
assertThat(orar.firstUnsatisfied(reg, 4)).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------- intervale per schema (AIP-22 treapta 2)
|
||||||
|
|
||||||
|
private static final String SLHDSA_CANONIC = "slh-dsa-sha2-128s";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void perSchemeIntervalsParseAndOnlyBiteOnTheirOwnGrid() {
|
||||||
|
// Falcon la fiecare al 32-lea bloc, SLH-DSA doar la fiecare al 128-lea, grilele numarate de la H0=64
|
||||||
|
final PqSchemeSchedule orar =
|
||||||
|
PqSchemeSchedule.parse("1000:" + FALCON + "/32+" + SLHDSA_CANONIC + "/128");
|
||||||
|
assertThat(orar.hasIntervals()).isTrue();
|
||||||
|
// schemesAt ramane setul intreg: cheile si armabilitatea privesc toate schemele
|
||||||
|
assertThat(orar.schemesAt(1000)).containsExactlyInAnyOrder(FALCON, SLHDSA_CANONIC);
|
||||||
|
// 64 + 32k dar nu 64 + 128k: doar Falcon
|
||||||
|
assertThat(orar.schemesRequiredForAnchor(1056, 64)).containsExactly(FALCON); // 1056-64 = 992 = 31*32
|
||||||
|
assertThat(orar.schemesRequiredForAnchor(1120, 64)).containsExactly(FALCON); // 1056 = 33*32, nu multiplu de 128
|
||||||
|
// 64 + 128k: amandoua
|
||||||
|
assertThat(orar.schemesRequiredForAnchor(1216, 64)).containsExactlyInAnyOrder(FALCON, SLHDSA_CANONIC); // 1152 = 9*128
|
||||||
|
// sub prima treapta (parintele 999 e sub 1000): nimic
|
||||||
|
assertThat(orar.schemesRequiredForAnchor(1000, 64)).isEmpty();
|
||||||
|
// pe granita: treapta se citeste la PARINTE (1000), ancora e 1001... care nu e pe grila 32 de la 64
|
||||||
|
assertThat(orar.schemesRequiredForAnchor(1001, 64)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withoutIntervalsEveryNamedSchemeIsRequiredAtEveryAnchorExactlyAsBefore() {
|
||||||
|
final PqSchemeSchedule orar = PqSchemeSchedule.parse("100:" + FALCON + "+" + SLHDSA);
|
||||||
|
assertThat(orar.hasIntervals()).isFalse();
|
||||||
|
for (final long h : new long[] {101, 133, 200, 999_999}) {
|
||||||
|
assertThat(orar.schemesRequiredForAnchor(h, 64)).isEqualTo(orar.schemesAt(h - 1));
|
||||||
|
}
|
||||||
|
// and a scheme WITHOUT an interval next to one WITH: the first at every anchor, the second on its grid
|
||||||
|
final PqSchemeSchedule mixt = PqSchemeSchedule.parse("100:" + FALCON + "+" + SLHDSA_CANONIC + "/128");
|
||||||
|
assertThat(mixt.schemesRequiredForAnchor(96 + 32, 96)).containsExactly(FALCON);
|
||||||
|
assertThat(mixt.schemesRequiredForAnchor(96 + 128, 96)).containsExactlyInAnyOrder(FALCON, SLHDSA_CANONIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void intervalsThatCannotLandOnAnchorsRefuseTheWholeSchedule() {
|
||||||
|
assertThatThrownBy(() -> PqSchemeSchedule.parse("100:" + FALCON + "/0"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("positive multiple");
|
||||||
|
assertThatThrownBy(() -> PqSchemeSchedule.parse("100:" + FALCON + "/48"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("48");
|
||||||
|
assertThatThrownBy(() -> PqSchemeSchedule.parse("100:" + FALCON + "/abc"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("not a number");
|
||||||
|
assertThatThrownBy(() -> PqSchemeSchedule.parse("100:" + FALCON + "/32+" + FALCON + "/64"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("repeats");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void beforeTheFirstStepMeansLegacyNotSomeDefaultScheme() {
|
void beforeTheFirstStepMeansLegacyNotSomeDefaultScheme() {
|
||||||
final PqSchemeSchedule orar = PqSchemeSchedule.parse("500:falcon-512");
|
final PqSchemeSchedule orar = PqSchemeSchedule.parse("500:falcon-512");
|
||||||
|
|||||||
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3: the ONE place the validator set changes form. After block H_id - 1 the vote
|
||||||
|
* tally holds the sorted post-quantum ids of the same members and open votes on addresses are gone;
|
||||||
|
* at every other block (and disarmed) the tally is untouched.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.validator.blockbased;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqRegistryV3;
|
||||||
|
import org.hyperledger.besu.consensus.common.validator.ValidatorVote;
|
||||||
|
import org.hyperledger.besu.consensus.common.validator.VoteType;
|
||||||
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class VoteTallyIdentityTest {
|
||||||
|
|
||||||
|
private static final long H_ID = 1500;
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void disarm() {
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theTallyTurnsIntoIdsAfterHidMinusOneAndDropsOpenVotes() throws Exception {
|
||||||
|
final JsonNode v;
|
||||||
|
try (InputStream in = getClass().getResourceAsStream("/aere/registry-v3/ok-epoch-a.json")) {
|
||||||
|
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);
|
||||||
|
final List<Address> addr = new ArrayList<>();
|
||||||
|
final List<Address> id = new ArrayList<>();
|
||||||
|
for (final PqRegistryV3.Row r : epoch.rows()) {
|
||||||
|
addr.add(Address.wrap(Bytes.wrap(r.ecdsaAddress())));
|
||||||
|
id.add(Address.wrap(Bytes.wrap(r.id())));
|
||||||
|
}
|
||||||
|
final TreeMap<Long, PqRegistryV3.Registry> epochs = new TreeMap<>();
|
||||||
|
epochs.put(epoch.bindHeight(), epoch);
|
||||||
|
|
||||||
|
// disarmed: nothing happens at any block
|
||||||
|
final VoteTally before = new VoteTally(addr);
|
||||||
|
before.translateToIds(H_ID - 1);
|
||||||
|
assertThat(before.getValidators()).containsExactlyElementsOf(new TreeSet<>(addr));
|
||||||
|
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(H_ID, epochs));
|
||||||
|
final VoteTally t = new VoteTally(addr);
|
||||||
|
final Address newcomer = Address.fromHexString("0x00000000000000000000000000000000000000cc");
|
||||||
|
t.addVote(new ValidatorVote(VoteType.ADD, addr.get(0), newcomer));
|
||||||
|
t.translateToIds(H_ID - 2);
|
||||||
|
assertThat(t.getValidators()).containsExactlyElementsOf(new TreeSet<>(addr));
|
||||||
|
t.translateToIds(H_ID - 1);
|
||||||
|
assertThat(t.getValidators()).containsExactlyElementsOf(new TreeSet<>(id));
|
||||||
|
// the open vote on an address is gone: two more votes for the newcomer would have reached the
|
||||||
|
// majority of three with it; after the switch they do not
|
||||||
|
t.addVote(new ValidatorVote(VoteType.ADD, id.get(1), newcomer));
|
||||||
|
assertThat(t.getValidators()).doesNotContain(newcomer);
|
||||||
|
t.translateToIds(H_ID);
|
||||||
|
assertThat(t.getValidators()).hasSize(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.hyperledger.besu.consensus.qbft.core.validation;
|
package org.hyperledger.besu.consensus.qbft.core.validation;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
|
||||||
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
|
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
|
||||||
import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
|
import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
|
||||||
import org.hyperledger.besu.consensus.qbft.core.messagewrappers.Commit;
|
import org.hyperledger.besu.consensus.qbft.core.messagewrappers.Commit;
|
||||||
@ -207,7 +208,9 @@ public class CommitValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Address commitSealCreator =
|
final Address commitSealCreator =
|
||||||
Util.signatureToAddress(payload.getCommitSeal(), expectedCommitDigest);
|
AerePqIdentity.author(
|
||||||
|
Util.signatureToAddress(payload.getCommitSeal(), expectedCommitDigest),
|
||||||
|
targetRound.getSequenceNumber());
|
||||||
|
|
||||||
if (!commitSealCreator.equals(signedPayload.getAuthor())) {
|
if (!commitSealCreator.equals(signedPayload.getAuthor())) {
|
||||||
LOG.info(
|
LOG.info(
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.hyperledger.besu.consensus.qbft.core.validation;
|
package org.hyperledger.besu.consensus.qbft.core.validation;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
|
||||||
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
||||||
import org.hyperledger.besu.consensus.common.bft.HybridSealSupport;
|
import org.hyperledger.besu.consensus.common.bft.HybridSealSupport;
|
||||||
import org.hyperledger.besu.consensus.common.bft.HybridSignerRegistry;
|
import org.hyperledger.besu.consensus.common.bft.HybridSignerRegistry;
|
||||||
@ -113,6 +114,8 @@ public final class PqCommitEnforcement {
|
|||||||
private final PqSchemeSchedule schemeSchedule;
|
private final PqSchemeSchedule schemeSchedule;
|
||||||
private final HybridSignerRegistry hybridRegistry;
|
private final HybridSignerRegistry hybridRegistry;
|
||||||
private final LongPredicate extrasRequiredAt;
|
private final LongPredicate extrasRequiredAt;
|
||||||
|
// AIP-22 stage 2: the origin of the anchor grids, read lazily so tests need no live config
|
||||||
|
private final java.util.function.LongSupplier anchorBlock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param armedFromBlock first block height (inclusive) at which enforcement applies; use
|
* @param armedFromBlock first block height (inclusive) at which enforcement applies; use
|
||||||
@ -179,11 +182,34 @@ public final class PqCommitEnforcement {
|
|||||||
final PqSchemeSchedule schemeSchedule,
|
final PqSchemeSchedule schemeSchedule,
|
||||||
final HybridSignerRegistry hybridRegistry,
|
final HybridSignerRegistry hybridRegistry,
|
||||||
final LongPredicate extrasRequiredAt) {
|
final LongPredicate extrasRequiredAt) {
|
||||||
|
this(
|
||||||
|
armedFromBlock,
|
||||||
|
registry,
|
||||||
|
schemeSchedule,
|
||||||
|
hybridRegistry,
|
||||||
|
extrasRequiredAt,
|
||||||
|
() -> PqAnchorProducer.config().anchorBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full constructor with an explicit anchor-grid origin (AIP-22 stage 2), for proofs that run
|
||||||
|
* a per-scheme interval schedule without the process-wide anchor configuration.
|
||||||
|
*
|
||||||
|
* @param anchorBlock supplies the chain's anchor activation height, origin of every grid
|
||||||
|
*/
|
||||||
|
public PqCommitEnforcement(
|
||||||
|
final long armedFromBlock,
|
||||||
|
final PqSignerRegistry registry,
|
||||||
|
final PqSchemeSchedule schemeSchedule,
|
||||||
|
final HybridSignerRegistry hybridRegistry,
|
||||||
|
final LongPredicate extrasRequiredAt,
|
||||||
|
final java.util.function.LongSupplier anchorBlock) {
|
||||||
this.armedFromBlock = armedFromBlock;
|
this.armedFromBlock = armedFromBlock;
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
this.schemeSchedule = schemeSchedule;
|
this.schemeSchedule = schemeSchedule;
|
||||||
this.hybridRegistry = hybridRegistry;
|
this.hybridRegistry = hybridRegistry;
|
||||||
this.extrasRequiredAt = extrasRequiredAt;
|
this.extrasRequiredAt = extrasRequiredAt;
|
||||||
|
this.anchorBlock = anchorBlock;
|
||||||
if ((schemeSchedule == null) != (hybridRegistry == null)) {
|
if ((schemeSchedule == null) != (hybridRegistry == null)) {
|
||||||
// Half a hybrid configuration is the shape that starts a node believing it enforces
|
// Half a hybrid configuration is the shape that starts a node believing it enforces
|
||||||
// something it does not. Refuse at construction, the same stance as every other AERE gate.
|
// something it does not. Refuse at construction, the same stance as every other AERE gate.
|
||||||
@ -243,6 +269,9 @@ public final class PqCommitEnforcement {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Every scheme the schedule names at this height, other than Falcon, must be present and valid.
|
* Every scheme the schedule names at this height, other than Falcon, must be present and valid.
|
||||||
|
* The height is an anchor PARENT (that is the only place extras are demanded), so the schemes
|
||||||
|
* are those the anchor at {@code height + 1} requires: with per-scheme intervals (AIP-22 stage
|
||||||
|
* 2) a commit at the parent of a Falcon-only anchor counts without an SLH-DSA seal.
|
||||||
*/
|
*/
|
||||||
private Optional<String> hybridRefusal(
|
private Optional<String> hybridRefusal(
|
||||||
final long height,
|
final long height,
|
||||||
@ -250,7 +279,8 @@ public final class PqCommitEnforcement {
|
|||||||
final Hash commitDigest,
|
final Hash commitDigest,
|
||||||
final FalconSeal falconSeal,
|
final FalconSeal falconSeal,
|
||||||
final List<SchemeSeal> extraSeals) {
|
final List<SchemeSeal> extraSeals) {
|
||||||
final Set<String> required = schemeSchedule.schemesAt(height);
|
final Set<String> required =
|
||||||
|
schemeSchedule.schemesRequiredForAnchor(height + 1L, anchorBlock.getAsLong());
|
||||||
for (final String schemeId : required) {
|
for (final String schemeId : required) {
|
||||||
if (SealSchemes.FALCON_512.id().equals(schemeId)) {
|
if (SealSchemes.FALCON_512.id().equals(schemeId)) {
|
||||||
continue; // already decided by the Falcon path above
|
continue; // already decided by the Falcon path above
|
||||||
@ -284,7 +314,7 @@ public final class PqCommitEnforcement {
|
|||||||
}
|
}
|
||||||
final Optional<byte[]> bound = hybridRegistry.address(found.getValidatorIndex());
|
final Optional<byte[]> bound = hybridRegistry.address(found.getValidatorIndex());
|
||||||
if (bound.isEmpty()
|
if (bound.isEmpty()
|
||||||
|| !Address.wrap(Bytes.wrap(bound.get())).equals(author)) {
|
|| !AerePqIdentity.author(Address.wrap(Bytes.wrap(bound.get())), height).equals(author)) {
|
||||||
return Optional.of(
|
return Optional.of(
|
||||||
"AERE HIBRID: index " + found.getValidatorIndex()
|
"AERE HIBRID: index " + found.getValidatorIndex()
|
||||||
+ " is not bound to the commit author " + author + " in the hybrid registry");
|
+ " is not bound to the commit author " + author + " in the hybrid registry");
|
||||||
|
|||||||
@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3: the identity switch, measured on real QBFT messages.
|
||||||
|
*
|
||||||
|
* Four upstream test nodes (QbftNodeList: a secp256k1 node key and a message factory each) get a
|
||||||
|
* registry v3 row each (a Falcon-512 key with its possession proof, and the ecdsa link signed by the
|
||||||
|
* node's own key). The validator set given to the upstream validators is the list of their IDS.
|
||||||
|
*
|
||||||
|
* What must hold, each with its negative:
|
||||||
|
* - armed, a Commit (and a Prepare) signed with the node's ECDSA key is accepted against the id set:
|
||||||
|
* the author is translated to the id, and the commit seal's creator is translated the same way;
|
||||||
|
* - DISARMED, the very same message is REFUSED against the id set (its author is an address): the
|
||||||
|
* translation, and nothing else, is what makes it pass;
|
||||||
|
* - armed but BELOW H_id, it is refused against ids and accepted against addresses;
|
||||||
|
* - a node with no row stays an address and is refused.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.qbft.core.validation;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
|
||||||
|
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.types.QbftBlockCodec;
|
||||||
|
import org.hyperledger.besu.crypto.SECPSignature;
|
||||||
|
import org.hyperledger.besu.crypto.SecureRandomProvider;
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class AereIdentityMessageTest {
|
||||||
|
|
||||||
|
private static final long CHAIN = 28001;
|
||||||
|
private static final long H_ID = 10;
|
||||||
|
private static final int N = 4;
|
||||||
|
|
||||||
|
private final Hash expectedHash = Hash.fromHexStringLenient("0x1");
|
||||||
|
private @Mock QbftBlockCodec qbftBlockCodec;
|
||||||
|
private QbftNodeList nodes;
|
||||||
|
private final List<Address> ids = new ArrayList<>();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void registryForTheNodes() {
|
||||||
|
nodes = QbftNodeList.createNodes(N, qbftBlockCodec);
|
||||||
|
final long bind = 5;
|
||||||
|
final List<PqRegistryV3.Row> rows = new ArrayList<>();
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
final SealScheme f = SealSchemes.FALCON_512;
|
||||||
|
final SealScheme.GeneratedPair p = f.generate(SecureRandomProvider.createSecureRandom());
|
||||||
|
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, f.wireId(), std);
|
||||||
|
final byte[] pop = f.sign(p.privateKey(), popMsg.toArrayUnsafe()).orElseThrow();
|
||||||
|
final TreeMap<Integer, PqRegistryV3.Key> keys = new TreeMap<>();
|
||||||
|
keys.put(f.wireId() & 0xff, new PqRegistryV3.Key(f, 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 =
|
||||||
|
nodes.getNode(i).getNodeKey().sign(PqRegistryV3.claimDigest(CHAIN, rh));
|
||||||
|
rows.add(
|
||||||
|
new PqRegistryV3.Row(
|
||||||
|
i, id, keys, null, Map.of(),
|
||||||
|
nodes.getNode(i).getAddress().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);
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(H_ID, epochs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void disarm() {
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Commit commit(final int node, final ConsensusRoundIdentifier round) {
|
||||||
|
final SECPSignature seal =
|
||||||
|
nodes.getNode(node).getNodeKey().sign(Bytes32.wrap(expectedHash.getBytes()));
|
||||||
|
return nodes.getMessageFactory(node).createCommit(round, expectedHash, seal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void armedACommitSignedByTheNodeKeyIsAcceptedAgainstTheIdSet() {
|
||||||
|
final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(H_ID, 0);
|
||||||
|
final CommitValidator v = new CommitValidator(ids, round, expectedHash, expectedHash);
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
final Commit c = commit(i, round);
|
||||||
|
assertThat(c.getAuthor()).isEqualTo(ids.get(i));
|
||||||
|
assertThat(v.validate(c)).as("node " + i).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void disarmedTheSameCommitIsRefusedAgainstTheIdSet() {
|
||||||
|
final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(H_ID, 0);
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
final CommitValidator v = new CommitValidator(ids, round, expectedHash, expectedHash);
|
||||||
|
final Commit c = commit(0, round);
|
||||||
|
assertThat(c.getAuthor()).isEqualTo(nodes.getNode(0).getAddress());
|
||||||
|
assertThat(v.validate(c)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void belowHidTheAuthorIsTheAddress() {
|
||||||
|
final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(H_ID - 1, 0);
|
||||||
|
final Commit c = commit(1, round);
|
||||||
|
assertThat(c.getAuthor()).isEqualTo(nodes.getNode(1).getAddress());
|
||||||
|
assertThat(new CommitValidator(ids, round, expectedHash, expectedHash).validate(c)).isFalse();
|
||||||
|
assertThat(
|
||||||
|
new CommitValidator(nodes.getNodeAddresses(), round, expectedHash, expectedHash)
|
||||||
|
.validate(c))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aNodeWithoutARowStaysAnAddressAndIsRefused() {
|
||||||
|
final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(H_ID + 3, 0);
|
||||||
|
final QbftNode stranger = QbftNode.create(qbftBlockCodec);
|
||||||
|
final SECPSignature seal = stranger.getNodeKey().sign(Bytes32.wrap(expectedHash.getBytes()));
|
||||||
|
final Commit c = stranger.getMessageFactory().createCommit(round, expectedHash, seal);
|
||||||
|
assertThat(c.getAuthor()).isEqualTo(stranger.getAddress());
|
||||||
|
assertThat(new CommitValidator(ids, round, expectedHash, expectedHash).validate(c)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void armedAPrepareIsAuthoredByTheId() {
|
||||||
|
final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(H_ID + 1, 0);
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
final Prepare p = nodes.getMessageFactory(i).createPrepare(round, expectedHash);
|
||||||
|
assertThat(p.getAuthor()).isEqualTo(ids.get(i));
|
||||||
|
assertThat(new PrepareValidator(ids, round, expectedHash).validate(p)).isTrue();
|
||||||
|
}
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
final Prepare p = nodes.getMessageFactory(2).createPrepare(round, expectedHash);
|
||||||
|
assertThat(new PrepareValidator(ids, round, expectedHash).validate(p)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -190,6 +190,45 @@ public class PqHybridEnforcementTest {
|
|||||||
.isEmpty();
|
.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================ intervale per schema (AIP-22 treapta 2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With SLH-DSA on the 128 grid (Falcon on the 32 grid), counted from an activation height of 64,
|
||||||
|
* a commit at the parent of a Falcon-only anchor counts with the Falcon seal alone, and a commit
|
||||||
|
* at the parent of a hybrid anchor does not. The two heights differ in nothing but the grid; the
|
||||||
|
* grid origin is the negative control (read against another origin, the two heights swap).
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void withPerSchemeIntervalsTheSlhSealIsDemandedOnlyAtParentsOfAnchorsOnItsGrid() {
|
||||||
|
final PqSchemeSchedule cuIntervale =
|
||||||
|
PqSchemeSchedule.parse(
|
||||||
|
H_ARMARE + ":" + SealSchemes.FALCON_512.id() + "/32+" + SealSchemes.SLH_DSA_128S.id() + "/128");
|
||||||
|
final long origine = 64L;
|
||||||
|
long ancoraHibrida = origine;
|
||||||
|
while (ancoraHibrida <= H_HIBRID) {
|
||||||
|
ancoraHibrida += 128L;
|
||||||
|
}
|
||||||
|
final long ancoraDoarFalcon = ancoraHibrida + 32L;
|
||||||
|
final PqCommitEnforcement e =
|
||||||
|
new PqCommitEnforcement(H_ARMARE, registruFalcon(), cuIntervale, registry, h -> true, () -> origine);
|
||||||
|
assertThat(e.refusal(ancoraDoarFalcon - 1, VALIDATOR_0, DIGEST, Optional.of(sigiliuFalcon(0)), List.of()))
|
||||||
|
.isEmpty();
|
||||||
|
final Optional<String> refusal =
|
||||||
|
e.refusal(ancoraHibrida - 1, VALIDATOR_0, DIGEST, Optional.of(sigiliuFalcon(0)), List.of());
|
||||||
|
assertThat(refusal).isPresent();
|
||||||
|
assertThat(refusal.get()).contains("carries no").contains(SealSchemes.SLH_DSA_128S.id());
|
||||||
|
assertThat(
|
||||||
|
e.refusal(
|
||||||
|
ancoraHibrida - 1, VALIDATOR_0, DIGEST, Optional.of(sigiliuFalcon(0)), List.of(sigiliuSlh(0, DIGEST))))
|
||||||
|
.isEmpty();
|
||||||
|
final PqCommitEnforcement altaOrigine =
|
||||||
|
new PqCommitEnforcement(H_ARMARE, registruFalcon(), cuIntervale, registry, h -> true, () -> origine + 32L);
|
||||||
|
assertThat(altaOrigine.refusal(ancoraHibrida - 1, VALIDATOR_0, DIGEST, Optional.of(sigiliuFalcon(0)), List.of()))
|
||||||
|
.isEmpty();
|
||||||
|
assertThat(altaOrigine.refusal(ancoraDoarFalcon - 1, VALIDATOR_0, DIGEST, Optional.of(sigiliuFalcon(0)), List.of()))
|
||||||
|
.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================ controalele negative
|
// ============================================================ controalele negative
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.hyperledger.besu.consensus.qbft.headervalidationrules;
|
package org.hyperledger.besu.consensus.qbft.headervalidationrules;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
||||||
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
||||||
@ -423,8 +424,21 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final HybridSealSupport hybrid = HybridSealSupport.instance();
|
final HybridSealSupport hybrid = HybridSealSupport.instance();
|
||||||
|
// Two sets, and they differ only with per-scheme intervals (AIP-22 stage 2). NAMED: every scheme
|
||||||
|
// the step at the parent height names; a seal of any other scheme is refused as today. REQUIRED:
|
||||||
|
// the named schemes whose grid THIS anchor height sits on, counted from the chain's anchor
|
||||||
|
// activation height; each must reach K. A seal of a named-but-not-required scheme is a surplus:
|
||||||
|
// verified and counted like any surplus seal, never ignored, never a reason to refuse.
|
||||||
|
final Set<String> namedSchemes = new HashSet<>();
|
||||||
final Set<String> requiredSchemes = new HashSet<>();
|
final Set<String> requiredSchemes = new HashSet<>();
|
||||||
hybrid.schedule().ifPresent(sch -> requiredSchemes.addAll(sch.schemesAt(parent.getNumber())));
|
hybrid
|
||||||
|
.schedule()
|
||||||
|
.ifPresent(
|
||||||
|
sch -> {
|
||||||
|
namedSchemes.addAll(sch.schemesAt(parent.getNumber()));
|
||||||
|
requiredSchemes.addAll(sch.schemesRequiredForAnchor(number, config.anchorBlock()));
|
||||||
|
});
|
||||||
|
namedSchemes.remove(SealSchemes.FALCON_512.id());
|
||||||
requiredSchemes.remove(SealSchemes.FALCON_512.id());
|
requiredSchemes.remove(SealSchemes.FALCON_512.id());
|
||||||
final Map<String, Integer> counted = new HashMap<>();
|
final Map<String, Integer> counted = new HashMap<>();
|
||||||
for (final SchemeSeal s : seals) {
|
for (final SchemeSeal s : seals) {
|
||||||
@ -432,14 +446,14 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final Optional<SealScheme> scheme = SealSchemes.byWireId(s.getSchemeWireId());
|
final Optional<SealScheme> scheme = SealSchemes.byWireId(s.getSchemeWireId());
|
||||||
if (scheme.isEmpty() || !requiredSchemes.contains(scheme.get().id())) {
|
if (scheme.isEmpty() || !namedSchemes.contains(scheme.get().id())) {
|
||||||
LOG.warn(
|
LOG.warn(
|
||||||
"AERE PQ ANCHOR (R2): block {} REJECTED - the v2 certificate carries a seal of scheme 0x{},"
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the v2 certificate carries a seal of scheme 0x{},"
|
||||||
+ " which the scheme schedule does not name at the parent height {} (named: {}).",
|
+ " which the scheme schedule does not name at the parent height {} (named: {}).",
|
||||||
number,
|
number,
|
||||||
Integer.toHexString(s.getSchemeWireId() & 0xff),
|
Integer.toHexString(s.getSchemeWireId() & 0xff),
|
||||||
parent.getNumber(),
|
parent.getNumber(),
|
||||||
requiredSchemes);
|
namedSchemes);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!falconIndices.contains(s.getValidatorIndex())) {
|
if (!falconIndices.contains(s.getValidatorIndex())) {
|
||||||
@ -452,7 +466,10 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final Optional<HybridSignerRegistry> registry = hybrid.registry();
|
final Optional<HybridSignerRegistry> registry = hybrid.registry();
|
||||||
if (registry.isEmpty()) {
|
// AERE AIP-22 stage 3b: from H_k the key comes from the v3 epoch in force at the PARENT (the height the
|
||||||
|
// Falcon key of the same certificate is taken at); the hybrid registry is then not the key set any more
|
||||||
|
final boolean v3Keys = AerePqIdentity.keysFromV3(parent.getNumber());
|
||||||
|
if (registry.isEmpty() && !v3Keys) {
|
||||||
LOG.warn(
|
LOG.warn(
|
||||||
"AERE PQ ANCHOR (R2): block {} REJECTED - the certificate carries {} seals and this node"
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the certificate carries {} seals and this node"
|
||||||
+ " has NO hybrid registry loaded, so it cannot verify them. A node that cannot"
|
+ " has NO hybrid registry loaded, so it cannot verify them. A node that cannot"
|
||||||
@ -461,7 +478,10 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
|
|||||||
scheme.get().id());
|
scheme.get().id());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final Optional<byte[]> pk = registry.get().publicKey(s.getValidatorIndex(), scheme.get().id());
|
final Optional<byte[]> pk =
|
||||||
|
v3Keys
|
||||||
|
? AerePqIdentity.v3KeyAt(parent.getNumber(), s.getValidatorIndex(), scheme.get().id())
|
||||||
|
: registry.get().publicKey(s.getValidatorIndex(), scheme.get().id());
|
||||||
if (pk.isEmpty()) {
|
if (pk.isEmpty()) {
|
||||||
LOG.warn(
|
LOG.warn(
|
||||||
"AERE PQ ANCHOR (R2): block {} REJECTED - the hybrid registry holds no {} key for index {}.",
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the hybrid registry holds no {} key for index {}.",
|
||||||
|
|||||||
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* AERE AIP-22 stage 3: the identity switch on the IMPORT path - the committed seals of a real header.
|
||||||
|
*
|
||||||
|
* Four node keys seal a QBFT header (BftExtraDataFixture signs each committed seal over the
|
||||||
|
* committed-seal hash of that very header, as a live committer does). The committers recovered from
|
||||||
|
* it, through BOTH places upstream recovers them (BftBlockInterface.getCommitters for the header
|
||||||
|
* rules, BftBlockHashing.recoverCommitterAddresses for the rest), must be the v3 ids from H_id, and
|
||||||
|
* the classical addresses one block below H_id or with the switch disarmed. That is exactly what lets
|
||||||
|
* BftCommitSealsValidationRule accept a header sealed by ids against a validator set of ids.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.qbft;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.BftBlockHashing;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.BftBlockInterface;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.BftExtraDataFixture;
|
||||||
|
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.crypto.SECPSignature;
|
||||||
|
import org.hyperledger.besu.crypto.SecureRandomProvider;
|
||||||
|
import org.hyperledger.besu.cryptoservices.NodeKey;
|
||||||
|
import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
|
||||||
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
|
||||||
|
import org.hyperledger.besu.ethereum.core.Util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class AereIdentityHeaderTest {
|
||||||
|
|
||||||
|
private static final long CHAIN = 28001;
|
||||||
|
private static final long H_ID = 20;
|
||||||
|
private final QbftExtraDataCodec codec = new QbftExtraDataCodec();
|
||||||
|
private final List<NodeKey> keys = new ArrayList<>();
|
||||||
|
private final List<Address> addr = new ArrayList<>();
|
||||||
|
private final List<Address> ids = new ArrayList<>();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void registry() {
|
||||||
|
final long bind = 10;
|
||||||
|
final List<PqRegistryV3.Row> rows = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
final NodeKey k = NodeKeyUtils.generate();
|
||||||
|
keys.add(k);
|
||||||
|
addr.add(Util.publicKeyToAddress(k.getPublicKey()));
|
||||||
|
final SealScheme f = SealSchemes.FALCON_512;
|
||||||
|
final SealScheme.GeneratedPair p = f.generate(SecureRandomProvider.createSecureRandom());
|
||||||
|
final byte[] std = new byte[897];
|
||||||
|
std[0] = 0x09;
|
||||||
|
System.arraycopy(p.publicRegistryForm(), 0, std, 1, 896);
|
||||||
|
final byte[] id = PqRegistryV3.idOf(std);
|
||||||
|
final byte[] pop =
|
||||||
|
f.sign(
|
||||||
|
p.privateKey(),
|
||||||
|
PqRegistryV3.popMessage(CHAIN, bind, 4, i, id, f.wireId(), std).toArrayUnsafe())
|
||||||
|
.orElseThrow();
|
||||||
|
final TreeMap<Integer, PqRegistryV3.Key> km = new TreeMap<>();
|
||||||
|
km.put(f.wireId() & 0xff, new PqRegistryV3.Key(f, std, pop));
|
||||||
|
final Bytes32 rh =
|
||||||
|
PqRegistryV3.rowHash(
|
||||||
|
CHAIN, bind, 4, new PqRegistryV3.Row(i, id, km, null, Map.of(), null, null));
|
||||||
|
final SECPSignature claim = k.sign(PqRegistryV3.claimDigest(CHAIN, rh));
|
||||||
|
rows.add(
|
||||||
|
new PqRegistryV3.Row(
|
||||||
|
i, id, km, null, Map.of(), addr.get(i).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, 4, r));
|
||||||
|
}
|
||||||
|
final Bytes32 root = PqRegistryV3.merkleRoot(hashes);
|
||||||
|
final TreeMap<Long, PqRegistryV3.Registry> epochs = new TreeMap<>();
|
||||||
|
epochs.put(
|
||||||
|
bind,
|
||||||
|
new PqRegistryV3.Registry(
|
||||||
|
CHAIN, bind, 4, rows, hashes, root, PqRegistryV3.registryHash(CHAIN, bind, 4, root)));
|
||||||
|
AerePqIdentity.install(new AerePqIdentity.Config(H_ID, epochs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void disarm() {
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A header at this height, listing these validators, sealed by all four keys. */
|
||||||
|
private BlockHeader sealedHeader(final long number, final List<Address> validators) {
|
||||||
|
final BlockHeaderTestFixture b = new BlockHeaderTestFixture();
|
||||||
|
b.number(number);
|
||||||
|
b.gasLimit(5000);
|
||||||
|
b.timestamp(6 * number);
|
||||||
|
b.coinbase(validators.get(0));
|
||||||
|
b.blockHeaderFunctions(BftBlockHeaderFunctions.forCommittedSeal(codec));
|
||||||
|
final BftExtraData ed =
|
||||||
|
BftExtraDataFixture.createExtraData(
|
||||||
|
b.buildHeader(),
|
||||||
|
Bytes.wrap(new byte[BftExtraDataCodec.EXTRA_VANITY_LENGTH]),
|
||||||
|
Optional.empty(),
|
||||||
|
new ArrayList<>(new TreeSet<>(validators)),
|
||||||
|
keys,
|
||||||
|
0,
|
||||||
|
codec);
|
||||||
|
b.extraData(codec.encode(ed));
|
||||||
|
return b.buildHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fromHidTheCommittersOfARealHeaderAreTheIds() {
|
||||||
|
final BlockHeader h = sealedHeader(H_ID, ids);
|
||||||
|
assertThat(new BftBlockInterface(codec).getCommitters(h)).containsExactlyElementsOf(ids);
|
||||||
|
assertThat(new BftBlockHashing(codec).recoverCommitterAddresses(h, codec.decode(h)))
|
||||||
|
.containsExactlyElementsOf(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void belowHidAndDisarmedTheyAreTheAddresses() {
|
||||||
|
final BlockHeader below = sealedHeader(H_ID - 1, addr);
|
||||||
|
assertThat(new BftBlockInterface(codec).getCommitters(below)).containsExactlyElementsOf(addr);
|
||||||
|
final BlockHeader at = sealedHeader(H_ID, ids);
|
||||||
|
AerePqIdentity.uninstall();
|
||||||
|
assertThat(new BftBlockInterface(codec).getCommitters(at)).containsExactlyElementsOf(addr);
|
||||||
|
assertThat(new BftBlockHashing(codec).recoverCommitterAddresses(at, codec.decode(at)))
|
||||||
|
.containsExactlyElementsOf(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -197,6 +197,40 @@ public class PqAnchorV2RulesTest {
|
|||||||
|
|
||||||
// --------------------------------------------------------------------------------------- R2
|
// --------------------------------------------------------------------------------------- R2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AIP-22 stage 2, per-scheme intervals: Falcon on the 32 grid and SLH-DSA on the 128 grid, both
|
||||||
|
* counted from H. At an anchor on the 128 grid the certificate must reach K SLH-DSA seals; at an
|
||||||
|
* anchor on the 32 grid only, K Falcon seals suffice, and SLH-DSA seals carried there anyway are a
|
||||||
|
* surplus that is VERIFIED (a tampered one still refuses), not ignored and not refused.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void r2WithPerSchemeIntervalsDemandsSlhDsaOnlyOnItsGridAndVerifiesSurplus() throws Exception {
|
||||||
|
armHybrid("0:falcon-512/32+" + SLH + "/128");
|
||||||
|
long hybridAnchor = H;
|
||||||
|
while (hybridAnchor <= V2) {
|
||||||
|
hybridAnchor += 128L;
|
||||||
|
}
|
||||||
|
final long falconOnlyAnchor = hybridAnchor + 32L;
|
||||||
|
final BlockHeader p1 = parentHeader(hybridAnchor - 1L);
|
||||||
|
assertThat(r2.validate(honestV2(hybridAnchor, p1, certificate(p1, K, 0)), p1, context())).isFalse();
|
||||||
|
assertThat(r2.validate(honestV2(hybridAnchor, p1, certificate(p1, K, K)), p1, context())).isTrue();
|
||||||
|
final BlockHeader p2 = parentHeader(falconOnlyAnchor - 1L);
|
||||||
|
assertThat(r2.validate(honestV2(falconOnlyAnchor, p2, certificate(p2, K, 0)), p2, context())).isTrue();
|
||||||
|
assertThat(r2.validate(honestV2(falconOnlyAnchor, p2, certificate(p2, K, K)), p2, context())).isTrue();
|
||||||
|
final List<SchemeSeal> surplus = new ArrayList<>(certificate(p2, K, K));
|
||||||
|
for (int i = 0; i < surplus.size(); i++) {
|
||||||
|
final SchemeSeal s = surplus.get(i);
|
||||||
|
if (s.getSchemeWireId() == SealSchemes.SLH_DSA_128S.wireId()) {
|
||||||
|
final byte[] b = s.getSignature().toArray();
|
||||||
|
b[b.length / 2] ^= 1;
|
||||||
|
surplus.set(i, new SchemeSeal(s.getSchemeWireId(), s.getValidatorIndex(), Bytes.wrap(b)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surplus.sort(PqAnchorV2.CANONICAL);
|
||||||
|
assertThat(r2.validate(honestV2(falconOnlyAnchor, p2, surplus), p2, context())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void r2AcceptsTheHonestV2HeaderWithKSealsPerScheme() {
|
public void r2AcceptsTheHonestV2HeaderWithKSealsPerScheme() {
|
||||||
final BlockHeader parent = parentHeader(V2 + 4L);
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user