Anchor corpus restaged from the published recipe itself (base tree plus the patches run-node.sh applies, not the working overlay): every file the patches change is here, modified upstream files carry their Apache 4(b) notices, one internal reference is gone; BASE.txt says what the directory holds

This commit is contained in:
Aere Network 2026-10-02 00:50:15 +03:00
parent 725aeebb65
commit f4b0a3ce08
86 changed files with 27276 additions and 12 deletions

View File

@ -5,6 +5,11 @@ anything else they overwrite, and silently delete, whatever upstream added in be
against a newer base, 26.7.1: 97 lines would be lost, among them the registration of the /liveness
and /readiness probes in BesuCommand.java, and nothing would fail.
Since 2026-10-02 this directory holds every source file that the published patches (patches/0001 and
0003 to 0008, in the order run-node.sh applies them) change or add, taken from the recipe itself:
the base tree with those patches applied. It is a reading copy; the patches remain the authoritative
form, and a check in the operator repository compares the two file by file.
IF YOU MOVE THE BASE, do not take whole files from here. Re-apply the same changes as a patch
against your own base: a patch fails loudly when upstream has moved, a whole file does not.

View File

@ -16,6 +16,7 @@ ed2e46aaf2e74661ad831dc31df0b6f795f28f4a94496a7d226feedf2a770fde consensus/comm
a6c2bbf05eab4173a749c67ef8b7bce6d184d8eac1b445acb847efd082dc6d62 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTags.java
290d94bb50f23666b131ec19659982c15f1e6cbaf8f8bfe128df9acb2f2f79fd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqHeaderSeal.java
348135b2e4d0a6a8ce91489c94fbfa77da2536196c13823676b33ea9d08f8fb9 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqIdentity.java
dfdb28fb2d5016f0cbb7e8ba2cd9ababf18c6530349ddad3bf1e615cb25cc703 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockHashing.java
c531f9e719c48d3d5da2800fdef4506bd167af4dad4e25af96b4353a4390067d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java
9fcf44789ddf6d5b80be7d44f0bec58af6ba8316733d49817e890f0e17b71223 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java
70fa067469a084ac225765da6a01619dec51130acc255b7194a1ad274d327141 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java
@ -37,16 +38,22 @@ f67a205036550f7d0f1bfed2abe7b89abd73e80614adff8d696b33c580269a6f consensus/comm
660ba13ad60c49e86cf9ee4ced2b6932a5422d988f85c5c23098a92b029f4d25 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.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
80104994f8e5f02353c4bcb25084c451fd422c51c49e6891b574d76c7adf135f 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
776d9a422bcd306ad92d561ddebbbc1835d4ef5b147aa19d781556298bb27be4 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowStore.java
fad72fb52b6f6198113b80f5f2729002066a10decf596cfe26e602b058d9c603 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealed.java
a235b3ec8c69665c525d7c74a084d589904879f5cd33cb87e1d1f38eabba8a15 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java
addba40c0d931a3ecfa3b2f0179311dacaa604884e3ebb1d79f958bd913f72e2 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SchemeSeal.java
285f6a7c1188a387fb1ac21f5ab809595ef48d936b40aced94047525e6def0a6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java
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
9021d608f62e1330cf9b421013a06f2141bae469ba9162a24ce66f749aa4b21a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreator.java
454eeca487505e70c7f0000f5401b6bf4530da1d5ce6bcb803427f1d08e3cb1c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java
610275a18c2519d84ffb91982d7b9006a42605bbb330071572859df49f183c2a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftMiningCoordinator.java
e89deb6f346c221affa717eafa464d497401b7b5a01c84c144dace42032879a9 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java
d8efb4d625233e18ef4764eaa29650a97b56e75af18d07607eda1dd484187f76 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/messagewrappers/BftMessage.java
4b0731ec053e313a9ea9afc78592f25b6e97e6d230ebb273532437f08da92e5a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/network/ValidatorPeers.java
478f9be60c1259481bfd7b5a687e23ada29b4b07d4cdf6f9d3bbc13584d384f8 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/payload/SignedData.java
1e0113154d79a91a07104bcc67a9a843847751b921c5f9b7cc96d7bc30a1452f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
15ab42221f35aeb0dfd468bcb840254f08ccfccb5ff36fba559ac1017beca5ee consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
51b7d889188793a307bccb297d983f9318548e6066ad6228e37170585eda665f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
@ -71,6 +78,8 @@ fa85313f90f2e92b338d9c3f6df4cb57a7583fe8c82417f19928a133932dfd18 consensus/comm
2cd85f1517fe9770558ffef6251600bce76609a96beb21f0a8f120412b2a935f 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
ed1cc6f617dc5a2f7801b6e8f6f039a035a2ffc9542b116262dac39bb70563dd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryV3Tool.java
8aff8079c93d1463111e303831dbef886568790e2cff8ade77801a2864ae355b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTally.java
5a1014676efeea6e29d0603b4e7dac2d7334840c2f1ba3885bdc1b13e337cd7c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/validator/blockbased/VoteTallyUpdater.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
@ -108,6 +117,7 @@ f260d284f936307ac4c31142cbea0359ded1197880b8a960ac27dd7beb5c3b53 consensus/comm
05423fc69ce0e4e94cda0d56a181a996320bd2166af3415f2d88102cf3b0ae21 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryV3Test.java
d7d675be2a5ee598ce74f58156383589a05b8e977c2cc790671e3a39c7728309 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
45f25de86a064b8fd8e16a2c2f2194fabe55b3fefaadfcdaad6e58e96c72c800 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealWindowPersistenceTest.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
e65ba92e288aebe768909b1fee5c0b8850be74b3658c6242e6abdb91409a9521 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqV2Fixture.java
@ -120,6 +130,8 @@ ae37d3525697bf84defd796bb8813193c78f81e7fa24a72f8f048285d52399e2 consensus/comm
d0ba00d95427ae4fb1c0a1eeb37bcd039fbfa5412957dc4b2da0725c0dea5028 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SlhDsaParallelSigningTest.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
5ed47d976d4aafbb55369e1b3e96e6ad5efc3bf7c68be99ddb0e0ef9dbbc2f32 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Proposal.java
394c557de273fa9fa9a5ad024e8a1982575ad756736aa045ffeb0bdc7dc8016c consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/RoundChange.java
a1405402e891189909a436abd9f83c863349fdcf01d3011542d805e2bb3911ed consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java
35d38accf0d22b6fdadeab4f2bce0cdac708f8115591594b1bb1fa385b227c5c consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java
536375fa2d9f27107d9907f34391413e052f2a8cd89611033fefe29b9edc7891 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java
@ -128,7 +140,8 @@ a1405402e891189909a436abd9f83c863349fdcf01d3011542d805e2bb3911ed consensus/qbft
e19b12e6db33cfb9bb1c2f22e321815130dfbcc5730bb505320f08a45a371093 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java
51eddf3cb8e37489abd35e6f4c36294a13ec76337bbbbfd54c07584085dceaac consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java
0a450699c702104dcef6bf8912b4d180f05d647288c75a9f78da6af3bd3d90f1 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java
80613a8ac11ab48dfcecce5690a28da7d66dd8332584fd221f2875cb33d8c9d0 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
4a87188f209904c19f2b393fd50559a05b2be3161b4582364bc1641002bb4dff consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
324a5607313f25adb377d633be1f221588f8dafaa47a3c9d1e84fc5697323090 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRoundFactory.java
4494f33ca028b60eaa093e6c24dd13706461e228f7f5a29e40e8917d1ef3b373 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java
eadc4a14e2f0fdf7c1d9667d67c59f709af7a86fccf51923de0248b73e223c21 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java
4c0811d182f5252bdde31560f514a518db58c8d0c82c748a6c0f5d68472470c6 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java
@ -141,6 +154,7 @@ afb487f8c1dd87e323fdc1153ecfdf8ec6cb5a10e0726783d4214c07a3fa9a95 consensus/qbft
3aaa572c81641ad9a3e9cef9460b9a82dc4faff8080b933ef0e835881cf30bb4 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PrepareValidator.java
2f0001ca7b302b38b81e51fee14120c385070290afc2754a75ec581d170b4943 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/ProposalPayloadValidator.java
afa427cea95acd4d1f54f6824d39fae4125a7d4e23a84785f10b84ae9fe756ad consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidator.java
a32033398fd0a680f5d47930bbc7910e9eb178cdbc091419c35cf2a17e04ec27 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/AereD434ListBoundsTest.java
e2107737579c16b73cbd2c9b50e90abcd6c910309286fb862cef780555ca43a5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/network/ProposalSealPlumbingTest.java
6da7d76a2ddc3dcab2a6d54d60615bb54182594f230e858cfd0c13b2626b76e0 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/network/RoundChangeSealPlumbingTest.java
36dd77cdea980e4de0090e86ae2ca75d1af62a9477fe056c7fdde06422617898 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayloadHybridTest.java
@ -176,6 +190,7 @@ dc9f9e862a11f0135d26976176a1d1adac3e84fd5f1e06572d8727262cb3374f consensus/qbft
24ee993a415eea57c80fe2c9b839ce20140bfcc580e269e7b1182f71e960c1f0 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java
ebf6da56130720ea95c64505959d64bfba46540ebf2139c3f71a7051d88837a1 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java
c870d39a19174328bc394658f4a365a61bd57fe7b0e28e6c5e4aaeb28bb509e5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java
1238bdf6968781d12c7c7da8d31e5cf8b281a3e09612d450dc8ebae208542b31 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftFinalStateImpl.java
cfb0aad408ab620f28b09914ece7d8d3d09f9e6bbb503070e28d9ef39c023dee consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/blockcreation/QbftBlockCreatorFactory.java
cbec6f4e280250ddefe5c79c453b59a07be6435e6f88d5614e92168c855e4693 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRule.java
e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRule.java
@ -186,6 +201,8 @@ a76a87987050ea6765e2a1f43d8f828ca8aec218c951e395e7acb62465e8b2fd consensus/qbft
2add2a7733668e11b50ddd64b05bb37db26a18284a636bbe720c2cb1f6aa28ed consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRule.java
e0f8afc69fef826c5ee8a95745a967b1049ce10b9d591696a4fc095fb28062cb consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealRule.java
8ac99e94c89f61f0281199cf369e282fc4f9fb1a3414a1650766e1d7548e76f5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java
99cc2db4894a35c1df3c10d52d0deeffc4d6bf2a219f07249f5baccedf3889e7 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/QbftJsonRpcMethods.java
24b52131993fc93c5f146cab8588067cdb6201e308c36eef455d0ea56460a13b consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinality.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
5a18c7fee308654d9557fc507ca7a64704bdac5d13835e7df47a3dfb41519902 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRuleTest.java
@ -201,12 +218,72 @@ a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft
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
8d0113eec652dc5e08600fb7fb07de3e08524a500ea6bdcf3fd0f46f3f1b38e6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqHeaderSealStage5Test.java
7ea99f24bc1c9131e8ef08f7a2c6f2a08af71ebee011a4cb88a367bd446b6c27 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java
5bf41ab0be06328fa1e18bf9c98bfd89b7b16384fe185d96b3385595dabe61b3 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetCode.java
4c96b06073ff3830f4e0292fc732ec3fb9e0ed50ef741dceedf755b50f18d5ae ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetStorageAt.java
8e649d2a1c47cd912f0782408da3f3af5537e8c670c12cba72dbb502809c848d ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionCount.java
41b5f7b0066ef6d8af25491814eeacf2bcbb29415f407020affde05b7d1916b7 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/query/BlockchainQueries.java
212e932427839cebf788cf90ffb7893f2905880219bbcd051ee0b9279810df35 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/jsonrpc/methods/AereGetPqFinalityTest.java
f76961f1c07629cd4d3061ba3b59167875cf517d7adc7ceb302894ef430baf52 datatypes/src/main/java/org/hyperledger/besu/datatypes/Address.java
cd815f46bd834f62f49551dd0dc44b9c0cf88d33311fc5928803949dc9964562 datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAlgorithm.java
d3658d8ccc923f1b890b80fa83673c15cc213615db0ac2a9e343cd18e931c471 datatypes/src/main/java/org/hyperledger/besu/datatypes/AerePqAuthorization.java
17560a53ed57b6c22b43a6223eb0f52289eb1565dad2c1fe4a8a5565426a07cf datatypes/src/main/java/org/hyperledger/besu/datatypes/Transaction.java
9858c5116ce32ee4ebfa247a5eaa2befe1f5ec6507f61c7115f04a49ba0ece5f datatypes/src/main/java/org/hyperledger/besu/datatypes/TransactionType.java
9e370c961785ad6a2b16b3fc377a5341a5e930136bf9a47a8e1079ae501d1b62 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java
4f2fe5b68e55276d62181a8694fc8dbd44f4680ca5c0a28a555425135563cc43 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java
b5394c8f60a8afc46c0e09b8198b9212e808c728050bc20808e4652d45019d39 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetCode.java
dfceadc9d220ef9b028ef30e09e12a2c44caffee1922b6aa98c95937862bc9f9 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetStorageAt.java
cd94a4c059dd5e79136544f9dc09163df722ecedd41b8552cd0cdc22ba6328d3 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionCount.java
ce03332291e5a575cede22e2885e561ae5d27e815551d7d4af3a93045949eabc ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionBaseResult.java
6a54e53e6f1569b0bc00617188b8d82c6d69fa2de13db6285af0c5e959d1eae7 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/websocket/subscription/pending/PendingTransactionDetailResult.java
2de0590a917378d7cd1cb2a4ab1a4f9d0ac27545c0e080a83417244718606ba5 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/query/BlockchainQueries.java
3ee087fc34b455e113ec9d2c53b5dbdb3574429fcf166f4c968a80120f70f0eb ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AereStateUnavailableIsAnErrorTest.java
6e99e9f399558abbd1d4fdb4b176ba413bf31cabb3c8aba1701c17ce5828ce8a ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/AereHeadStateRetryTest.java
bf6617807ee0dd03ed98e6f1a73d6d11366880c2f742f34e73ef4d74a4f01180 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java
5d5032dbdad0f3dd15539b3ee6676cf3fd2bb0381d0db17d8abea2051b5c2c09 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionDecoder.java
95a30107a67e12d0f45df74365fd533bde15fab76036451590539a73714b0b84 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/AerePqTransactionEncoder.java
53f750f62dbf256a3d25fab595d651dd3c3872c207659aa839ab8403df4cf2ad ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionDecoder.java
2aa829c45cb5902f8467218244680ebc3f87162e6be8897b39d286813046dc23 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/TransactionEncoder.java
b3f5feddcae2e74f3b2517785a51e61998817fba4977d5f5a3fb9fbb48038af8 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AerePqTransactionSupport.java
10df69ba73a9847ea5b93d2fc2bc1c2dbf238b8b76d713568678403ea6edbd87 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java
73f66fae03826866c74e87bd262e7bac79ce3db814de782dc52b9263ce156059 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java
c19d72f93d09f945a6c5a0144125f8501cc0cb0d0749cd08e8d8ca091ad40dcc ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java
f2257781bf894f96f0fd67bc8f5d97fb8df181a95029147a9e594b068ca04cf2 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/WithdrawalsValidator.java
b3c6811983da5ce74f75a0fccdd87a2385881fa99210cdb0484d357ac0d844ab ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/AereBlockStmEngine.java
20d842d8413034e506c6ab22288a9e6870f0fb8a83362ec5adecb70b49f6af07 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/AereBlockStmPreprocessing.java
bd5d69c4ec63c212f364fa7fbb3ad658983654e772148bedb17c8028ce24540e ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/AereBlockStmProcessor.java
fb4a98f4edf9e8c362ce241e44771f6c8db8c34f54b8e4efb026ba3747b3975e ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/BaseReader.java
ae98ca304a11bfc7e051f5bfaf43b4270dca40dbae47f931e3d186358a87cb03 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/BlockStmStats.java
85b13b2561f7541b6bb1187a2d6c07a8aafdd447bb04ad712fe540703f94f5a1 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/FlatBaseSnapshot.java
cdc1bd5ce7bdeecc5328885622f7b21028c2d3d5bf4b60d5da13d8e7819dc984 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvAccount.java
e74fcdcf9fa8874c0eaf7e4467be0a31351faf096bb4147fe0d8702068fa2bd6 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvBaseLayer.java
e3fa43a8ac48028631d63d6e1a13f32c27a14430b52d1a1d89d9c2a454d954a3 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvContext.java
6e83f1158908ac8262f5de81be72a3adbc7e35f5dce57db576437ba924fdd1e3 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvKey.java
1e6dbfb45124b5cad58ee760fd394ccd12f8d514508eb533d6fbb1ee7c4f94b7 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/MvMemory.java
a1422099123d6644e53129c9caa59bca7cb54669c5a07438f53bb8cd065c89b2 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/blockstm/Scheduler.java
26a80db3a78556e874f6176e82c5e86475ba55fb574660a8a856806479ccd1c9 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/feemarket/LondonFeeMarket.java
10c888f52c388ec4ce8767ff435a70ef14c341eaa632acb10f8e75622e9ca3ae ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockDataGenerator.java
cbd5d4e986818ad65259e2bf3c46b1fff1bbec96a5513212a1b2ef1448c18cb7 ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TransactionTestFixture.java
f3ba58dda5335659912c7bf9c336ee00b2b5645ed7f868ffa9955425cb2ced7d ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/AerePqTransactionTest.java
01ac1387d8a7d155a56274c2929b0f129053c78b2602137a7bdd00c8a99009f6 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AereD432GasCapUnsignedTest.java
aa4999574b158ed3a9d738d7439033d543ed0a748792e0e60d1eb8ca8af84427 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AereD433BftWithdrawalsTest.java
816c47aba04623a15e48ece16b440d6d9e16d23031841c22f9159830862d6dd6 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/feemarket/LondonFeeMarketAereFloorTest.java
d34dd8c46ad4a394e8b15a2155c32f89883a0879c1cf7f1976f081938a3f56d3 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AereBlockStmBenchmarkTest.java
6f0119bcafb5247995b4399f766d4adc9275669fe3ca08fbe2df5b01800624e3 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/parallelization/AereBlockStmSmokeTest.java
f94acc1cd8aac6679c39c74f7d23375ba19781e9bfd1d14b8c8a9e4314384cc1 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeers.java
62abf412b67808f0648b1af363a376e567ba7796cb1c4aa93617adc3f2484b8e ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/BlockPropagationManager.java
3b690b72e80a0f9eaf868a3fa3f99ace42dad74dc30b2c98b9f3ca5a023917c0 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadHeadersStep.java
7f611966d74f52ef2f6d55bb5882ffb72b10abf1985df2b01f03aad4a6e2baf1 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/PropagatedBlockAdoptionGate.java
5a8187cc12bb1a16a9b77956c4e1c5d8c088a4e65164179c427cd7b2c2f24b7f ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/state/SyncState.java
e068fc39b4f645d1ccdd778acdf77983869a73428513c11ce587547391e36ce3 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransaction.java
eb8d2305070adcf0424ab9884c8f09d9bc95a1662a4cf8775a136e3a314fa181 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java
76ce97047c7988c106969f69fbdcba6941657c482ea1e060b995e7ce0556c108 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/state/SyncStateTest.java
cc003a07cc4cf27e20ebf249ae8d84b8f514d1c24e55dfaab00fea3c345e1a0c ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/BaseTransactionPoolTest.java
55e6c022442a970411bb677a1d0b27a4a7f8174b4fabd200de5b10393731b201 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java
e39a09c49cec66fa2766470ba29bbccced6f42ecb7f95df39ccf5086b666510c ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLPInput.java
2190cd3df779c0b04ca1ca09098ea1a99df07dab6659a2d4cac058f34c27e495 evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java
eeeb63160aba1f889725d628b8623630baf227a82e367844b6d24619d8e83e7d evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java
72866e4744d34a120345a1ed3a0699292fd4a63d04d6256b3a7337738441e7bf evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java
417134787ef4f01af0674bb8e641a7eef3c701e1d9d4e8a1c807faab4bb97d99 evm/src/main/java/org/hyperledger/besu/evm/precompile/AereFalconSupport.java
c59d8c714155b347fe64bb378715e71670a8bd42180a97d08156e6c20caa359c evm/src/main/java/org/hyperledger/besu/evm/precompile/Falcon1024PrecompiledContract.java
6487810e9b167eb6c0a7046a5226e40936116771efb91013d6c2eb9559c60f97 evm/src/main/java/org/hyperledger/besu/evm/precompile/Falcon512PrecompiledContract.java
42e4d1442d3478eac8c3177e35318872c365a173d4a16f529356e52944901d50 evm/src/main/java/org/hyperledger/besu/evm/precompile/MLDSA44PrecompiledContract.java
d17d9fde238d5d6527380d9829d1b52cd3c7c76c9874c6172e4ddf1dcc26acba evm/src/main/java/org/hyperledger/besu/evm/precompile/MainnetPrecompiledContracts.java
c3b208adc9e81229eb9194fbedd40186aeccf43b8039827604d00169878d6690 evm/src/main/java/org/hyperledger/besu/evm/precompile/P256VerifyPrecompiledContract.java
c4ef4c11f13c7387b79c5d6532299f540d8c4cb82c98a865be2d01053bd5d35e evm/src/main/java/org/hyperledger/besu/evm/precompile/SHAKE256PrecompiledContract.java
976daf6471e817ca668942aa07adfc7ede7d59b4c941d0bbb91d02a25fa6fbb4 evm/src/main/java/org/hyperledger/besu/evm/precompile/SLHDSA128sPrecompiledContract.java

View File

@ -0,0 +1,144 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.consensus.common.bft;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder;
import org.hyperledger.besu.ethereum.core.Util;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.tuweni.bytes.Bytes;
/** The Bft block hashing. */
public class BftBlockHashing {
private final BftExtraDataCodec bftExtraDataCodec;
/**
* Instantiates a new Bft block hashing.
*
* @param bftExtraDataCodec the bft extra data codec
*/
public BftBlockHashing(final BftExtraDataCodec bftExtraDataCodec) {
this.bftExtraDataCodec = bftExtraDataCodec;
}
/**
* Constructs a hash of the block header suitable for signing as a committed seal. The extra data
* in the hash uses an empty list for the committed seals.
*
* @param header The header for which a proposer seal is to be calculated (with or without extra
* data)
* @param bftExtraData The extra data block which is to be inserted to the header once seal is
* calculated
* @return the hash of the header including the validator and proposer seal in the extra data
*/
public Hash calculateDataHashForCommittedSeal(
final BlockHeader header, final BftExtraData bftExtraData) {
return Hash.hash(
serializeHeader(
header,
() -> bftExtraDataCodec.encodeWithoutCommitSeals(bftExtraData),
bftExtraDataCodec));
}
/**
* Calculate data hash for committed seal hash.
*
* @param header the header
* @return the hash
*/
public Hash calculateDataHashForCommittedSeal(final BlockHeader header) {
final BftExtraData bftExtraData = bftExtraDataCodec.decode(header);
return Hash.hash(
serializeHeader(
header,
() -> bftExtraDataCodec.encodeWithoutCommitSeals(bftExtraData),
bftExtraDataCodec));
}
/**
* Constructs a hash of the block header, but omits the committerSeals and sets round number to 0
* (as these change on each of the potentially circulated blocks at the current chain height).
*
* @param header The header for which a block hash is to be calculated
* @return the hash of the header to be used when referencing the header on the blockchain
*/
public Hash calculateHashOfBftBlockOnchain(final BlockHeader header) {
final BftExtraData bftExtraData = bftExtraDataCodec.decode(header);
return Hash.hash(
serializeHeader(
header,
() -> bftExtraDataCodec.encodeWithoutCommitSealsAndRoundNumber(bftExtraData),
bftExtraDataCodec));
}
/**
* Recovers the {@link Address} for each validator that contributed a committed seal to the block.
*
* @param header the block header that was signed by the committed seals
* @param bftExtraData the parsed {@link BftExtraData} from the header
* @return the addresses of validators that provided a committed seal
*/
public List<Address> recoverCommitterAddresses(
final BlockHeader header, final BftExtraData bftExtraData) {
final Hash committerHash = calculateDataHashForCommittedSeal(header, bftExtraData);
return bftExtraData.getSeals().stream()
.map(p -> AerePqIdentity.author(Util.signatureToAddress(p, committerHash), header.getNumber()))
.collect(Collectors.toList());
}
/**
* Serialize header bytes.
*
* @param header the header
* @param extraDataSerializer the extra data serializer
* @param bftExtraDataCodec the bft extra data codec
* @return the bytes
*/
public static Bytes serializeHeader(
final BlockHeader header,
final Supplier<Bytes> extraDataSerializer,
final BftExtraDataCodec bftExtraDataCodec) {
// create a block header which is a copy of the header supplied as parameter except of the
// extraData field
final BlockHeaderBuilder builder = BlockHeaderBuilder.fromHeader(header);
builder.blockHeaderFunctions(BftBlockHeaderFunctions.forOnchainBlock(bftExtraDataCodec));
// set the extraData field using the supplied extraDataSerializer if the block height is not 0
if (header.getNumber() == BlockHeader.GENESIS_BLOCK_NUMBER) {
builder.extraData(header.getExtraData());
} else {
builder.extraData(extraDataSerializer.get());
}
final BytesValueRLPOutput out = new BytesValueRLPOutput();
builder.buildBlockHeader().writeTo(out);
return out.encoded();
}
}

View File

@ -69,11 +69,39 @@ public final class PqSealCache {
private static final Logger LOG = LoggerFactory.getLogger(PqSealCache.class);
/** How many distinct block hashes are retained. Several rounds at one height each add one. */
private static final int MAX_ENTRIES = 1024;
/**
* AIP-21 (AERE, 2026-09-18): how far below the highest seen height entries are kept, in blocks. The
* consensus default stays 256 (what the producer needs is the parent); a node that serves
* aere_getPqFinality may keep more through the system property, at ~7 kB per block for ten seals.
* Read once, never below 256, never a consensus value: two nodes with different windows import
* the same chain.
*/
public static final String PROPERTY_FINALITY_WINDOW = "aere.pq.finality.window";
private static final long DEFAULT_HEIGHT_WINDOW = 256L;
/** How far below the highest seen height entries are kept, in blocks. */
private static final long HEIGHT_WINDOW = 256L;
private static final long HEIGHT_WINDOW = readWindow();
/** How many distinct block hashes are retained. Several rounds at one height each add one. */
private static final int MAX_ENTRIES = (int) Math.max(1024L, Math.min(HEIGHT_WINDOW * 4L, 1 << 20));
private static long readWindow() {
final Long v = Long.getLong(PROPERTY_FINALITY_WINDOW);
if (v == null) {
return DEFAULT_HEIGHT_WINDOW;
}
return Math.max(DEFAULT_HEIGHT_WINDOW, Math.min(v, 1L << 18));
}
/**
* The retention window in blocks, as this node runs it.
*
* @return the window
*/
public long retentionBlocks() {
return HEIGHT_WINDOW;
}
private static final PqSealCache INSTANCE = new PqSealCache();
@ -90,6 +118,16 @@ public final class PqSealCache {
private Hash persistedHash;
private int persistedCount = -1;
// AIP-21, SECOND HALF (2026-09-18): the finality window survives a restart. Heard heights are
// appended to a segmented journal (PqSealWindowStore) when the NEXT height begins, and read back,
// re-verified seal by seal against the canonical chain, at startup. Null = journal off.
private volatile Path windowDirectory;
private volatile long windowPrunedSegment = -1L;
private long windowJournaledThrough = -1L;
/** Heights restored at startup at most, newest first, whatever the configured window. */
private static final long MAX_RESTORED_HEIGHTS = 4096L;
private PqSealCache() {}
/**
@ -121,6 +159,9 @@ public final class PqSealCache {
}
this.persistenceChainId = chainId;
this.persistenceFile = PqSealStore.fileIn(dataDirectory);
this.windowPrunedSegment = -1L;
this.windowDirectory =
PqSealWindowStore.enabled() ? PqSealWindowStore.directoryIn(dataDirectory) : null;
LOG.info(
"AERE PERSISTENTA-SIGILII: heard seals for the chain head are kept in {} (chainId={}). "
+ "Every seal read back is re-verified against the anchored registry before it is used.",
@ -133,6 +174,7 @@ public final class PqSealCache {
*/
public void disablePersistence() {
this.persistenceFile = null;
this.windowDirectory = null;
synchronized (writeLock) {
persistedBlock = -1L;
persistedHash = null;
@ -226,6 +268,7 @@ public final class PqSealCache {
public void record(
final long blockNumber, final Hash onchainBlockHash, final Collection<FalconSeal> seals) {
final List<FalconSeal> toPersist;
List<PqSealWindowStore.Heard> windowToJournal = List.of();
synchronized (this) {
if (onchainBlockHash == null || seals == null || seals.isEmpty()) {
return;
@ -238,6 +281,9 @@ public final class PqSealCache {
}
}
if (blockNumber > highestSeen) {
// AIP-21 second half: a NEW height begins, so what was heard for the heights below it is as
// complete as this node will ever hold it. It goes to the window journal, outside this lock.
windowToJournal = windowSnapshotBelow(blockNumber);
highestSeen = blockNumber;
}
prune();
@ -251,6 +297,8 @@ public final class PqSealCache {
// Deliberately OUTSIDE the map lock: a file write must never be able to stall a caller that only
// wants to read the cache.
persist(blockNumber, onchainBlockHash, toPersist);
// the head file first (liveness depends on it), the window journal after (best effort)
persistWindow(windowToJournal);
}
private void persist(
@ -396,6 +444,7 @@ public final class PqSealCache {
public synchronized void clear() {
byHash.clear();
highestSeen = -1L;
windowJournaledThrough = -1L; // what clear() forgets, the journal mark forgets too
synchronized (writeLock) {
persistedBlock = -1L;
persistedHash = null;
@ -403,6 +452,157 @@ public final class PqSealCache {
}
}
/**
* AIP-21, second half. Everything heard BELOW a height that has just begun and not yet handed to
* the window journal. Caller holds the map lock.
*
* @param newHeight the height that has just begun
* @return the heard blocks to journal, possibly empty
*/
private List<PqSealWindowStore.Heard> windowSnapshotBelow(final long newHeight) {
if (windowDirectory == null) {
return List.of();
}
final List<PqSealWindowStore.Heard> out = new ArrayList<>();
for (final Map.Entry<Hash, Entry> e : byHash.entrySet()) {
final long n = e.getValue().blockNumber;
if (n > windowJournaledThrough && n < newHeight && !e.getValue().seals.isEmpty()) {
out.add(PqSealWindowStore.heard(n, e.getKey().getBytes(), e.getValue().seals.values()));
}
}
windowJournaledThrough = Math.max(windowJournaledThrough, newHeight - 1L);
return out;
}
private void persistWindow(final List<PqSealWindowStore.Heard> heard) {
final Path directory = windowDirectory;
if (directory == null || heard.isEmpty()) {
return;
}
synchronized (writeLock) {
try {
long highest = -1L;
for (final PqSealWindowStore.Heard h : heard) {
PqSealWindowStore.append(directory, persistenceChainId, h);
highest = Math.max(highest, h.blockNumber());
}
final long segment = highest / PqSealWindowStore.SEGMENT_BLOCKS;
if (segment != windowPrunedSegment) {
PqSealWindowStore.pruneSegments(directory, highest, HEIGHT_WINDOW);
windowPrunedSegment = segment;
}
} catch (final Exception e) {
// A disk fault must not be able to stop this node from taking part in consensus. It costs the
// answer aere_getPqFinality gives for these blocks after a restart, and nothing else.
LOG.warn(
"AERE FEREASTRA-SIGILII: could not append {} heard block(s) to the window journal {} ({}). "
+ "This node keeps working; after a restart it answers 'unavailable' for them.",
heard.size(),
directory,
e.toString());
}
}
}
/**
* AIP-21, second half. Bring the finality window back after a restart. Nothing in the journal is
* believed: a frame counts only if its hash is the CANONICAL hash this node's own chain holds at
* that height, and each seal only if it verifies, against the registry epoch bound at that height,
* over the commit message rebuilt from that canonical hash. Never throws.
*
* @param headNumber this node's chain head
* @param canonicalHashAt the canonical on-chain hash at a height, empty when the chain holds none
* @param registry the anchored registry seals are verified against
* @return how many verified seals entered the window
*/
public int restoreWindowFromDisk(
final long headNumber,
final java.util.function.LongFunction<java.util.Optional<Hash>> canonicalHashAt,
final PqSignerRegistry registry) {
final Path directory = windowDirectory;
if (directory == null || canonicalHashAt == null || registry == null) {
return 0;
}
int restored = 0;
try {
final long from = Math.max(0L, headNumber - Math.min(HEIGHT_WINDOW, MAX_RESTORED_HEIGHTS));
final List<PqSealWindowStore.Heard> frames =
PqSealWindowStore.readAll(directory, persistenceChainId, from, headNumber);
long highestInJournal = -1L;
int notCanonical = 0;
int rejected = 0;
final LinkedHashMap<Hash, Entry> verified = new LinkedHashMap<>();
for (final PqSealWindowStore.Heard frame : frames) {
final long n = frame.blockNumber();
highestInJournal = Math.max(highestInJournal, n);
final java.util.Optional<Hash> canonical = canonicalHashAt.apply(n);
if (canonical == null
|| canonical.isEmpty()
|| !canonical.get().getBytes().equals(frame.blockHash())) {
notCanonical++;
continue;
}
final org.apache.tuweni.bytes.Bytes32 message =
PqAnchor.commitMessage(persistenceChainId, n, frame.blockHash());
for (final FalconSeal seal : frame.seals()) {
if (seal == null
|| seal.getValidatorIndex() < 0
|| seal.getSignature() == null
|| !registry.verifyAtHistoric(
n, seal.getValidatorIndex(), message, seal.getSignature())) {
rejected++;
continue;
}
final Entry entry = verified.computeIfAbsent(canonical.get(), unused -> new Entry(n));
if (entry.seals.putIfAbsent(seal.getValidatorIndex(), seal) == null) {
restored++;
}
}
}
synchronized (this) {
// Oldest first, so the size cap still evicts the oldest: the restored heights go in BEFORE
// what this node already holds (its head, restored a moment ago from the head file).
final LinkedHashMap<Hash, Entry> held = new LinkedHashMap<>(byHash);
byHash.clear();
byHash.putAll(verified);
for (final Map.Entry<Hash, Entry> e : held.entrySet()) {
final Entry target = byHash.get(e.getKey());
if (target == null) {
byHash.put(e.getKey(), e.getValue());
} else {
e.getValue().seals.forEach(target.seals::putIfAbsent);
e.getValue().extras.forEach(target.extras::putIfAbsent);
}
}
for (final Entry entry : verified.values()) {
if (entry.blockNumber > highestSeen) {
highestSeen = entry.blockNumber;
}
}
windowJournaledThrough = Math.max(windowJournaledThrough, highestInJournal);
prune();
}
LOG.info(
"AERE FEREASTRA-SIGILII: {} verified seal(s) over {} block(s) restored into the finality "
+ "window from {} ({} frame(s) read, {} not canonical on this chain, {} seal(s) FAILED "
+ "verification and were dropped).",
restored,
verified.size(),
directory,
frames.size(),
notCanonical,
rejected);
} catch (final RuntimeException e) {
LOG.warn(
"AERE FEREASTRA-SIGILII: restoring the finality window from {} failed unexpectedly ({}); "
+ "the window starts empty, exactly as before this journal existed.",
directory,
e.toString());
return 0;
}
return restored;
}
private void prune() {
final Iterator<Map.Entry<Hash, Entry>> it = byHash.entrySet().iterator();
while (it.hasNext()) {

View File

@ -0,0 +1,387 @@
/*
* Copyright contributors to Besu / AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.consensus.common.bft;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.rlp.RLPInput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* AIP-21, SECOND HALF (2026-09-18): the finality window on disk.
*
* <p>The first half gave every validator a window of heard Falcon seals ({@link PqSealCache}) and a
* method that reads it back ({@code aere_getPqFinality}). The window lived in memory only, so a node
* that restarted answered {@code unavailable} for every block it had heard before the restart. This
* class is the journal that carries the window across a restart.
*
* <p>SHAPE. One directory under the node's data directory, {@value #DIRECTORY_NAME}, holding
* SEGMENT files of {@value #SEGMENT_BLOCKS} heights each ({@code w-<height / 64>.rlp}). A segment is
* a sequence of FRAMES: {@code length(4, big endian) || payload || crc32(payload)(4)}, the payload
* being the RLP list {@code [DOMAIN, chainId, blockNumber, blockHash, certificate]} with the
* certificate encoded exactly as {@link PqSealStore} and the header encode it. Frames are only ever
* APPENDED, one per heard block, when the next height begins; whole segments below the window are
* deleted. The footprint is therefore bounded by the window, whatever the age of the node.
*
* <p>WHY THIS IS NOT A8 IN ANOTHER COAT, the same argument as {@link PqSealStore}: a seal is
* SELF-VERIFYING. Nothing read from this directory is believed. {@link PqSealCache} re-verifies
* every seal against the registry epoch bound at its height, over the message rebuilt from the
* CANONICAL hash this process reads from its own chain at that height; a frame for a hash that is
* not canonical is dropped before a single signature is looked at. A forged journal can obtain the
* empty window an absent journal already gives, and nothing else.
*
* <p>WHAT A FAULT COSTS. Every method here is written not to throw on bad input: a torn tail (the
* node died mid-append), a flipped byte, a directory where a file should be, a journal from another
* chain. A bad frame costs that frame and what FOLLOWS it in the same segment (a frame boundary
* cannot be trusted after a bad length), never the node. Writes are best effort and unsynced: the
* head file of {@link PqSealStore} is the one liveness depends on, and it keeps its fsync.
*/
public final class PqSealWindowStore {
private static final Logger LOG = LoggerFactory.getLogger(PqSealWindowStore.class);
/** Directory name under the node's data directory. */
public static final String DIRECTORY_NAME = "aere-pq-seal-window";
/** Domain separator of a frame payload. */
public static final String DOMAIN = "AERE-PQ-SEALWINDOW-1";
private static final Bytes DOMAIN_BYTES = Bytes.wrap(DOMAIN.getBytes(StandardCharsets.US_ASCII));
/** Heights per segment file. */
public static final long SEGMENT_BLOCKS = 64L;
/** A segment larger than this is ignored whole. */
public static final long MAX_SEGMENT_BYTES = 64L * 1024L * 1024L;
/** A frame payload larger than this ends the reading of its segment. */
public static final int MAX_FRAME_BYTES = 4 * 1024 * 1024;
/** More seals than this in one frame is not a certificate. */
public static final int MAX_SEALS = 1024;
/** System property: {@code false} turns the journal off (the window is then memory only). */
public static final String PROPERTY_PERSIST = "aere.pq.finality.persist";
private static final Pattern SEGMENT_NAME = Pattern.compile("^w-(\\d{1,18})\\.rlp$");
private PqSealWindowStore() {}
/**
* One heard block as the journal holds it. Unverified: the caller verifies.
*
* @param blockNumber the height
* @param blockHash the round-independent on-chain hash the seals were heard for
* @param seals the heard seals, index-sorted
*/
public record Heard(long blockNumber, Bytes blockHash, List<FalconSeal> seals) {}
/**
* Whether the journal is enabled for this process.
*
* @return false only when {@value #PROPERTY_PERSIST} is {@code false}
*/
public static boolean enabled() {
return !"false".equalsIgnoreCase(System.getProperty(PROPERTY_PERSIST));
}
/**
* The journal directory of a node.
*
* @param dataDirectory the node's data directory
* @return the directory the segments live in
*/
public static Path directoryIn(final Path dataDirectory) {
return dataDirectory.resolve(DIRECTORY_NAME);
}
/**
* The segment file a height belongs to.
*
* @param directory the journal directory
* @param blockNumber the height
* @return the segment path
*/
public static Path segmentFor(final Path directory, final long blockNumber) {
return directory.resolve("w-" + (blockNumber / SEGMENT_BLOCKS) + ".rlp");
}
/**
* Encode one frame payload.
*
* @param chainId the chain id
* @param heard the heard block
* @return the RLP payload
*/
public static byte[] encode(final long chainId, final Heard heard) {
final BytesValueRLPOutput out = new BytesValueRLPOutput();
out.startList();
out.writeBytes(DOMAIN_BYTES);
out.writeLongScalar(chainId);
out.writeLongScalar(heard.blockNumber());
out.writeBytes(heard.blockHash());
PqAnchor.writeCertificate(out, heard.seals());
out.endList();
return out.encoded().toArrayUnsafe();
}
/**
* Append one heard block to its segment. Best effort, unsynced.
*
* @param directory the journal directory
* @param chainId the chain id
* @param heard the heard block
* @throws IOException when the file system refuses; the caller logs and carries on
*/
public static void append(final Path directory, final long chainId, final Heard heard)
throws IOException {
Files.createDirectories(directory);
final byte[] payload = encode(chainId, heard);
final CRC32 crc = new CRC32();
crc.update(payload, 0, payload.length);
final ByteBuffer frame = ByteBuffer.allocate(4 + payload.length + 4);
frame.putInt(payload.length);
frame.put(payload);
frame.putInt((int) crc.getValue());
frame.flip();
try (FileChannel channel =
FileChannel.open(
segmentFor(directory, heard.blockNumber()),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.APPEND)) {
while (frame.hasRemaining()) {
channel.write(frame);
}
}
}
/**
* Delete every segment that lies WHOLLY below the window.
*
* @param directory the journal directory
* @param highestBlock the highest height heard
* @param window the retention window in blocks
* @return how many segment files were deleted
*/
public static int pruneSegments(final Path directory, final long highestBlock, final long window) {
if (directory == null || !Files.isDirectory(directory)) {
return 0;
}
final long oldestKept = Math.max(0L, highestBlock - window);
int deleted = 0;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (final Path file : stream) {
final Matcher m = SEGMENT_NAME.matcher(file.getFileName().toString());
if (!m.matches()) {
continue;
}
final long lastHeightOfSegment = (Long.parseLong(m.group(1)) + 1L) * SEGMENT_BLOCKS - 1L;
if (lastHeightOfSegment < oldestKept) {
try {
Files.deleteIfExists(file);
deleted++;
} catch (final IOException e) {
LOG.debug("AERE FEREASTRA-SIGILII: could not delete {} ({}).", file, e.toString());
}
}
}
} catch (final IOException | RuntimeException e) {
LOG.debug("AERE FEREASTRA-SIGILII: could not list {} ({}).", directory, e.toString());
}
return deleted;
}
/**
* Read every frame for a height range, oldest first. Never throws; never verifies.
*
* @param directory the journal directory
* @param chainId the chain id this node runs
* @param fromBlock the lowest height wanted, inclusive
* @param toBlock the highest height wanted, inclusive
* @return the frames that decoded, for this chain, within the range
*/
public static List<Heard> readAll(
final Path directory, final long chainId, final long fromBlock, final long toBlock) {
final List<Heard> out = new ArrayList<>();
if (directory == null || toBlock < fromBlock || !Files.isDirectory(directory)) {
return out;
}
final List<Path> segments = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (final Path file : stream) {
final Matcher m = SEGMENT_NAME.matcher(file.getFileName().toString());
if (!m.matches()) {
continue;
}
final long first = Long.parseLong(m.group(1)) * SEGMENT_BLOCKS;
final long last = first + SEGMENT_BLOCKS - 1L;
if (last >= fromBlock && first <= toBlock) {
segments.add(file);
}
}
} catch (final IOException | RuntimeException e) {
LOG.warn(
"AERE FEREASTRA-SIGILII: the window journal {} could not be listed ({}); IGNORED, the "
+ "window starts empty exactly as it did before this journal existed.",
directory,
e.toString());
return out;
}
for (final Path segment : segments) {
readSegment(segment, chainId, fromBlock, toBlock, out);
}
out.sort(Comparator.comparingLong(Heard::blockNumber));
return out;
}
private static void readSegment(
final Path segment,
final long chainId,
final long fromBlock,
final long toBlock,
final List<Heard> out) {
final byte[] raw;
try {
if (!Files.isRegularFile(segment)) {
return;
}
final long size = Files.size(segment);
if (size <= 0 || size > MAX_SEGMENT_BYTES) {
LOG.warn(
"AERE FEREASTRA-SIGILII: segment {} has an implausible size ({} bytes); IGNORED.",
segment,
size);
return;
}
raw = Files.readAllBytes(segment);
} catch (final IOException | RuntimeException e) {
LOG.warn(
"AERE FEREASTRA-SIGILII: segment {} could not be read ({}); IGNORED.",
segment,
e.toString());
return;
}
final ByteBuffer buffer = ByteBuffer.wrap(raw);
int frames = 0;
while (buffer.remaining() >= 8) {
final int length = buffer.getInt();
if (length <= 0 || length > MAX_FRAME_BYTES || buffer.remaining() < length + 4) {
LOG.info(
"AERE FEREASTRA-SIGILII: segment {} ends in a torn or implausible frame after {} good "
+ "frame(s); what follows it is dropped.",
segment,
frames);
return;
}
final byte[] payload = new byte[length];
buffer.get(payload);
final int storedCrc = buffer.getInt();
final CRC32 crc = new CRC32();
crc.update(payload, 0, payload.length);
if ((int) crc.getValue() != storedCrc) {
LOG.warn(
"AERE FEREASTRA-SIGILII: segment {} holds a frame whose checksum does not match after "
+ "{} good frame(s); it and what follows it are dropped.",
segment,
frames);
return;
}
frames++;
final Heard heard = decode(payload, chainId);
if (heard != null && heard.blockNumber() >= fromBlock && heard.blockNumber() <= toBlock) {
out.add(heard);
}
}
}
private static Heard decode(final byte[] payload, final long chainId) {
try {
final RLPInput in = new BytesValueRLPInput(Bytes.wrap(payload), false);
in.enterList();
if (!DOMAIN_BYTES.equals(in.readBytes())) {
return null;
}
if (in.readLongScalar() != chainId) {
return null;
}
final long blockNumber = in.readLongScalar();
final Bytes blockHash = in.readBytes();
if (blockNumber < 0 || blockHash.size() != 32) {
return null;
}
final List<FalconSeal> seals = new ArrayList<>();
in.enterList();
while (!in.isEndOfCurrentList()) {
if (seals.size() >= MAX_SEALS) {
return null;
}
in.enterList();
final int index = in.readIntScalar();
final Bytes signature = in.readBytes();
in.leaveList();
seals.add(new FalconSeal(index, signature));
}
in.leaveList();
in.leaveList();
return new Heard(blockNumber, blockHash, seals);
} catch (final RuntimeException e) {
return null;
}
}
/**
* Count the frames of a journal, for tests and for the operator's curiosity. Never throws.
*
* @param directory the journal directory
* @param chainId the chain id
* @return how many frames decode for this chain, at any height
*/
public static int frameCount(final Path directory, final long chainId) {
return readAll(directory, chainId, 0L, Long.MAX_VALUE).size();
}
/**
* The seals of one frame collection, for callers that hold a plain collection.
*
* @param blockNumber the height
* @param blockHash the on-chain hash
* @param seals the heard seals
* @return the frame value, seals index-sorted
*/
public static Heard heard(
final long blockNumber, final Bytes blockHash, final Collection<FalconSeal> seals) {
return new Heard(blockNumber, blockHash, PqAnchor.sortedByIndex(new ArrayList<>(seals)));
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-bft-prevrandao-block-creation.patch.
*/
package org.hyperledger.besu.consensus.common.bft.blockcreation;
import org.hyperledger.besu.config.BftConfigOptions;
import org.hyperledger.besu.consensus.common.ForksSchedule;
import org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions;
import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec;
import org.hyperledger.besu.consensus.common.bft.BftHelpers;
import org.hyperledger.besu.consensus.common.bft.BftProtocolSchedule;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.blockcreation.AbstractBlockCreator;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder;
import org.hyperledger.besu.ethereum.core.MiningConfiguration;
import org.hyperledger.besu.ethereum.core.SealableBlockHeader;
import org.hyperledger.besu.ethereum.eth.manager.EthScheduler;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import java.util.Collections;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes32;
/** The Bft block creator. */
// This class is responsible for creating a block without committer seals (basically it was just
// too hard to coordinate with the state machine).
public class BftBlockCreator extends AbstractBlockCreator {
private final BftExtraDataCodec bftExtraDataCodec;
/**
* Instantiates a new Bft block creator.
*
* @param miningConfiguration the mining parameters
* @param forksSchedule the forks schedule
* @param localAddress the local address
* @param extraDataCalculator the extra data calculator
* @param transactionPool the pending transactions
* @param protocolContext the protocol context
* @param protocolSchedule the protocol schedule
* @param bftExtraDataCodec the bft extra data codec
* @param ethScheduler the scheduler for asynchronous block creation tasks
*/
public BftBlockCreator(
final MiningConfiguration miningConfiguration,
final ForksSchedule<? extends BftConfigOptions> forksSchedule,
final Address localAddress,
final ExtraDataCalculator extraDataCalculator,
final TransactionPool transactionPool,
final ProtocolContext protocolContext,
final ProtocolSchedule protocolSchedule,
final BftExtraDataCodec bftExtraDataCodec,
final EthScheduler ethScheduler) {
super(
miningConfiguration.setCoinbase(localAddress),
miningBeneficiaryCalculator(protocolSchedule),
extraDataCalculator,
transactionPool,
protocolContext,
protocolSchedule,
ethScheduler);
this.bftExtraDataCodec = bftExtraDataCodec;
}
@Override
public BlockCreationResult createBlock(final long timestamp, final BlockHeader parentHeader) {
ProtocolSpec protocolSpec =
((BftProtocolSchedule) protocolSchedule)
.getByBlockNumberOrTimestamp(parentHeader.getNumber() + 1, timestamp);
if (protocolSpec.getWithdrawalsProcessor().isPresent()) {
return createEmptyWithdrawalsBlock(timestamp, parentHeader);
} else {
return createBlock(
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of(Bytes32.wrap(BftHelpers.EXPECTED_MIX_HASH.getBytes())),
Optional.empty(),
Optional.empty(),
timestamp,
true,
parentHeader);
}
}
/**
* Construct a mining beneficiary calculator using the given protocolSchedule
*
* @param protocolSchedule protocol schedule
* @return mining beneficiary calculator
*/
public static MiningBeneficiaryCalculator miningBeneficiaryCalculator(
final ProtocolSchedule protocolSchedule) {
return (blockTimestamp, pendingHeader) -> {
BlockHeader newBlockHeader =
BlockHeaderBuilder.createDefault()
.coinbase(pendingHeader.getCoinbase())
.buildBlockHeader();
ProtocolSpec protocolSpec =
((BftProtocolSchedule) protocolSchedule)
.getByBlockNumberOrTimestamp(pendingHeader.getNumber(), blockTimestamp);
return protocolSpec.getMiningBeneficiaryCalculator().calculateBeneficiary(newBlockHeader);
};
}
@Override
public BlockCreationResult createEmptyWithdrawalsBlock(
final long timestamp, final BlockHeader parentHeader) {
return createBlock(
Optional.empty(),
Optional.empty(),
Optional.of(Collections.emptyList()),
Optional.of(Bytes32.wrap(BftHelpers.EXPECTED_MIX_HASH.getBytes())),
Optional.empty(),
Optional.empty(),
timestamp,
true,
parentHeader);
}
@Override
protected BlockHeader createFinalBlockHeader(final SealableBlockHeader sealableBlockHeader) {
final BlockHeaderBuilder builder =
BlockHeaderBuilder.create()
.populateFrom(sealableBlockHeader)
.mixHash(BftHelpers.EXPECTED_MIX_HASH)
.nonce(0L)
.blockHeaderFunctions(BftBlockHeaderFunctions.forCommittedSeal(bftExtraDataCodec));
return builder.buildBlockHeader();
}
}

View File

@ -0,0 +1,279 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-bft-coordinator-lifecycle-sync.patch.
*/
package org.hyperledger.besu.consensus.common.bft.blockcreation;
import org.hyperledger.besu.consensus.common.bft.BftEventQueue;
import org.hyperledger.besu.consensus.common.bft.BftExecutors;
import org.hyperledger.besu.consensus.common.bft.BftProcessor;
import org.hyperledger.besu.consensus.common.bft.events.NewChainHead;
import org.hyperledger.besu.consensus.common.bft.statemachine.BftEventHandler;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.chain.BlockAddedEvent;
import org.hyperledger.besu.ethereum.chain.BlockAddedObserver;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.eth.sync.state.SyncState;
import org.hyperledger.besu.plugin.services.BesuEvents;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** The Bft mining coordinator. */
public class BftMiningCoordinator implements MiningCoordinator, BlockAddedObserver {
private enum State {
/** Idle state. */
IDLE,
/** Running state. */
RUNNING,
/** Stopped state. */
STOPPED,
/** Paused state. */
PAUSED,
}
private static final Logger LOG = LoggerFactory.getLogger(BftMiningCoordinator.class);
private final BftEventHandler eventHandler;
private final BftProcessor bftProcessor;
private final BftBlockCreatorFactory<?> blockCreatorFactory;
/** The Blockchain. */
protected final Blockchain blockchain;
private final BftEventQueue eventQueue;
private final BftExecutors bftExecutors;
private long blockAddedObserverId;
private final AtomicReference<State> state = new AtomicReference<>(State.PAUSED);
private SyncState syncState;
/**
* Instantiates a new Bft mining coordinator.
*
* @param bftExecutors the bft executors
* @param eventHandler the event handler
* @param bftProcessor the bft processor
* @param blockCreatorFactory the block creator factory
* @param blockchain the blockchain
* @param eventQueue the event queue
*/
public BftMiningCoordinator(
final BftExecutors bftExecutors,
final BftEventHandler eventHandler,
final BftProcessor bftProcessor,
final BftBlockCreatorFactory<?> blockCreatorFactory,
final Blockchain blockchain,
final BftEventQueue eventQueue) {
this.bftExecutors = bftExecutors;
this.eventHandler = eventHandler;
this.bftProcessor = bftProcessor;
this.blockCreatorFactory = blockCreatorFactory;
this.eventQueue = eventQueue;
this.blockchain = blockchain;
}
/**
* Instantiates a new Bft mining coordinator.
*
* @param bftExecutors the bft executors
* @param eventHandler the event handler
* @param bftProcessor the bft processor
* @param blockCreatorFactory the block creator factory
* @param blockchain the blockchain
* @param eventQueue the event queue
* @param syncState the sync state
*/
public BftMiningCoordinator(
final BftExecutors bftExecutors,
final BftEventHandler eventHandler,
final BftProcessor bftProcessor,
final BftBlockCreatorFactory<?> blockCreatorFactory,
final Blockchain blockchain,
final BftEventQueue eventQueue,
final SyncState syncState) {
this.bftExecutors = bftExecutors;
this.eventHandler = eventHandler;
this.bftProcessor = bftProcessor;
this.blockCreatorFactory = blockCreatorFactory;
this.eventQueue = eventQueue;
this.blockchain = blockchain;
this.syncState = syncState;
}
@Override
public void start() {
if (state.compareAndSet(State.IDLE, State.RUNNING)
|| state.compareAndSet(State.STOPPED, State.RUNNING)) {
bftProcessor.start();
bftExecutors.start();
blockAddedObserverId = blockchain.observeBlockAdded(this);
eventHandler.start();
bftExecutors.executeBftProcessor(bftProcessor);
}
}
@Override
public void stop() {
if (state.compareAndSet(State.RUNNING, State.STOPPED)) {
blockchain.removeObserver(blockAddedObserverId);
bftProcessor.stop();
// Make sure the processor has stopped before shutting down the executors
try {
bftProcessor.awaitStop();
} catch (final InterruptedException e) {
LOG.debug("Interrupted while waiting for BftProcessor to stop.", e);
Thread.currentThread().interrupt();
}
eventHandler.stop();
bftExecutors.stop();
}
}
@Override
public void subscribe() {
if (syncState == null) {
return;
}
syncState.subscribeSyncStatus(
syncStatus -> {
if (syncState.syncTarget().isPresent() || !syncState.isInitialSyncPhaseDone()) {
// We're syncing so stop doing other stuff
LOG.info("Stopping BFT mining coordinator while we are syncing");
stop();
} else {
LOG.info("Starting BFT mining coordinator following sync");
enable();
start();
}
});
syncState.subscribeCompletionReached(
new BesuEvents.InitialSyncCompletionListener() {
@Override
public void onInitialSyncCompleted() {
LOG.info("Starting BFT mining coordinator following initial sync");
enable();
start();
}
@Override
public void onInitialSyncRestart() {
// Nothing to do. The mining coordinator won't be started until
// sync has completed.
}
});
}
@Override
public void awaitStop() throws InterruptedException {
bftExecutors.awaitStop();
}
@Override
public synchronized boolean enable() {
// AERE D-434 (81a600236a, issue #10877): synchronized with start()/stop() so the multi-step
// state checks below cannot interleave with a concurrent lifecycle transition.
// Return true if we're already running or idle, or successfully switch to idle
if (state.get() == State.RUNNING
|| state.get() == State.IDLE
|| state.compareAndSet(State.PAUSED, State.IDLE)) {
return true;
}
return false;
}
@Override
public synchronized boolean disable() {
// AERE D-434 (81a600236a, issue #10877): synchronized with start()/stop() so the multi-step
// state checks below cannot interleave with a concurrent lifecycle transition.
if (state.get() == State.PAUSED
|| state.compareAndSet(State.IDLE, State.PAUSED)
|| state.compareAndSet(State.RUNNING, State.PAUSED)) {
return true;
}
return false;
}
@Override
public boolean isMining() {
return state.get() == State.RUNNING;
}
@Override
public Wei getMinTransactionGasPrice() {
return blockCreatorFactory.getMinTransactionGasPrice();
}
@Override
public Wei getMinPriorityFeePerGas() {
return blockCreatorFactory.getMinPriorityFeePerGas();
}
@Override
public Optional<Address> getCoinbase() {
return Optional.of(blockCreatorFactory.getLocalAddress());
}
@Override
public Optional<Block> createBlock(
final BlockHeader parentHeader,
final List<Transaction> transactions,
final List<BlockHeader> ommers) {
// One-off block creation has not been implemented
return Optional.empty();
}
@Override
public Optional<Block> createBlock(final BlockHeader parentHeader, final long timestamp) {
// One-off block creation has not been implemented
return Optional.empty();
}
@Override
public void changeTargetGasLimit(final Long targetGasLimit) {
blockCreatorFactory.changeTargetGasLimit(targetGasLimit);
}
@Override
public void onBlockAdded(final BlockAddedEvent event) {
if (event.isNewCanonicalHead()) {
LOG.trace("New canonical head detected");
eventQueue.add(new NewChainHead(event.getHeader()));
}
}
@Override
public void removeObserver() {
blockchain.removeObserver(blockAddedObserverId);
}
}

View File

@ -0,0 +1,154 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-bft-bounded-message-lists.patch.
*/
package org.hyperledger.besu.consensus.common.bft.messagewrappers;
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
import org.hyperledger.besu.consensus.common.bft.payload.Authored;
import org.hyperledger.besu.consensus.common.bft.payload.Payload;
import org.hyperledger.besu.consensus.common.bft.payload.RoundSpecific;
import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
import org.hyperledger.besu.crypto.SECPSignature;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.rlp.RLPInput;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import org.apache.tuweni.bytes.Bytes;
/**
* The Bft message.
*
* @param <P> the type of Payload
*/
public class BftMessage<P extends Payload> implements Authored, RoundSpecific {
/**
* Maximum number of entries permitted in any embedded list (round-changes, prepares) when
* decoding a BFT message. AERE D-434: ported from upstream hyperledger/besu dd4101c9ec.
*/
public static final int MAX_LIST_ENTRIES = 512;
private final SignedData<P> payload;
/**
* Instantiates a new Bft message.
*
* @param payload the payload
*/
public BftMessage(final SignedData<P> payload) {
this.payload = payload;
}
@Override
public Address getAuthor() {
return payload.getAuthor();
}
@Override
public ConsensusRoundIdentifier getRoundIdentifier() {
return payload.getPayload().getRoundIdentifier();
}
/**
* Encode.
*
* @return the bytes
*/
public Bytes encode() {
final BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
payload.writeTo(rlpOut);
return rlpOut.encoded();
}
/**
* Gets signed payload.
*
* @return the signed payload
*/
public SignedData<P> getSignedPayload() {
return payload;
}
/**
* Gets message type.
*
* @return the message type
*/
public int getMessageType() {
return payload.getPayload().getMessageType();
}
/**
* Gets payload.
*
* @return the payload
*/
protected P getPayload() {
return payload.getPayload();
}
/**
* Read payload.
*
* @param <T> the type parameter of Payload
* @param rlpInput the rlp input
* @param decoder the decoder
* @return the signed data
*/
protected static <T extends Payload> SignedData<T> readPayload(
final RLPInput rlpInput, final Function<RLPInput, T> decoder) {
rlpInput.enterList();
final T unsignedMessageData = decoder.apply(rlpInput);
final SECPSignature signature =
rlpInput.readBytes((SignatureAlgorithmFactory.getInstance()::decodeSignature));
rlpInput.leaveList();
return SignedData.create(unsignedMessageData, signature);
}
@Override
public String toString() {
return new StringJoiner(", ", BftMessage.class.getSimpleName() + "[", "]")
.add("payload=" + payload)
.toString();
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final BftMessage<?> that = (BftMessage<?>) o;
return Objects.equals(payload, that.payload);
}
@Override
public int hashCode() {
return Objects.hash(payload);
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.consensus.common.bft.network;
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection;
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection.PeerNotConnected;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Responsible for tracking the network peers which have a connection to this node, then
* multicasting packets to ONLY the peers which have been identified as being validators.
*/
public class ValidatorPeers implements ValidatorMulticaster, PeerConnectionTracker {
private static final Logger LOG = LoggerFactory.getLogger(ValidatorPeers.class);
// It's possible for multiple connections between peers to exist for brief periods, so map each
// address to a set of connections
private final Map<Address, Set<PeerConnection>> connectionsByAddress = new ConcurrentHashMap<>();
private final ValidatorProvider validatorProvider;
private final String protocolName;
/**
* Instantiates a new Validator peers.
*
* @param validatorProvider the validator provider
* @param protocolName the protocol name
*/
public ValidatorPeers(final ValidatorProvider validatorProvider, final String protocolName) {
this.validatorProvider = validatorProvider;
this.protocolName = protocolName;
}
@Override
public void add(final PeerConnection newConnection) {
final Address peerAddress = newConnection.getPeerInfo().getAddress();
final Set<PeerConnection> connections =
connectionsByAddress.computeIfAbsent(
peerAddress, (k) -> Collections.newSetFromMap(new ConcurrentHashMap<>()));
connections.add(newConnection);
}
@Override
public void remove(final PeerConnection removedConnection) {
final Address peerAddress = removedConnection.getPeerInfo().getAddress();
final Set<PeerConnection> connections = connectionsByAddress.get(peerAddress);
if (connections == null) {
return;
}
connections.remove(removedConnection);
}
@Override
public void send(final MessageData message) {
sendMessageToSpecificAddresses(getLatestValidators(), message);
}
@Override
public void send(final MessageData message, final Collection<Address> denylist) {
final Collection<Address> includedValidators =
getLatestValidators().stream()
.filter(a -> !denylist.contains(a))
.collect(Collectors.toSet());
sendMessageToSpecificAddresses(includedValidators, message);
}
private void sendMessageToSpecificAddresses(
final Collection<Address> recipients, final MessageData message) {
LOG.trace(
"Sending message to peers messageCode={} recipients={}", message.getCode(), recipients);
// AERE AIP-22 stage 3: from H_id the recipients are ids; they are reached through the node-key
// address their v3 row lists as transport, or through every peer when a row lists none
final java.util.Optional<Collection<Address>> targets =
AerePqIdentity.transportTargets(recipients);
(targets.isPresent()
? targets.get().stream().map(connectionsByAddress::get).filter(Objects::nonNull)
: connectionsByAddress.values().stream())
.flatMap(Set::stream)
.distinct()
.forEach(
connection -> {
try {
connection.sendForProtocol(protocolName, message);
} catch (final PeerNotConnected peerNotConnected) {
LOG.trace(
"Lost connection to a validator. remoteAddress={} peerInfo={}",
connection.getRemoteAddress(),
connection.getPeerInfo());
}
});
}
private Collection<Address> getLatestValidators() {
return validatorProvider.getValidatorsAtHead();
}
}

View File

@ -0,0 +1,150 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.consensus.common.bft.payload;
import org.hyperledger.besu.consensus.common.bft.AerePqFinality;
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
import org.hyperledger.besu.crypto.SECPSignature;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.core.Util;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.rlp.RLPOutput;
import java.math.BigInteger;
import java.util.Objects;
import java.util.StringJoiner;
import org.apache.tuweni.bytes.Bytes;
/**
* The type Signed data.
*
* @param <M> the type parameter of Payload
*/
public class SignedData<M extends Payload> implements Authored {
private static final BigInteger HALF_CURVE_ORDER =
SignatureAlgorithmFactory.getInstance().getHalfCurveOrder();
private final Address sender;
private final SECPSignature signature;
private final M unsignedPayload;
/**
* Create signed data.
*
* @param <T> the type parameter
* @param payload the payload
* @param signature the signature
* @return the signed data
* @throws IllegalArgumentException if the signature is invalid
*/
public static <T extends Payload> SignedData<T> create(
final T payload, final SECPSignature signature) {
final Hash msgHash = payload.hashForSignature();
// AERE AIP-22 stage 3: from H_id the author is the post-quantum id of the signer's v3 row
// AERE AIP-22 stage 4: from H_fin the author is the id of the row the message's post-quantum seal index names, and the
// envelope's ECDSA signature is not read for authorship (a message without a post-quantum seal has no author then)
final long aereHeight = payload.getRoundIdentifier().getSequenceNumber();
return new SignedData<>(
payload,
AerePqFinality.active(aereHeight)
? AerePqFinality.authorFromSeal(payload, aereHeight)
: AerePqIdentity.author(Util.signatureToAddress(signature, msgHash), aereHeight),
signature);
}
private SignedData(final M unsignedPayload, final Address sender, final SECPSignature signature) {
if (!(signature.getS().compareTo(HALF_CURVE_ORDER) <= 0)) {
throw new IllegalArgumentException("Signature is invalid");
}
this.unsignedPayload = unsignedPayload;
this.sender = sender;
this.signature = signature;
}
@Override
public Address getAuthor() {
return sender;
}
/**
* Gets payload.
*
* @return the payload
*/
public M getPayload() {
return unsignedPayload;
}
/**
* Write to.
*
* @param output the output
*/
public void writeTo(final RLPOutput output) {
output.startList();
unsignedPayload.writeTo(output);
output.writeBytes(signature.encodedBytes());
output.endList();
}
/**
* Encode.
*
* @return the bytes
*/
public Bytes encode() {
final BytesValueRLPOutput rlpEncode = new BytesValueRLPOutput();
writeTo(rlpEncode);
return rlpEncode.encoded();
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final SignedData<?> that = (SignedData<?>) o;
return Objects.equals(sender, that.sender)
&& Objects.equals(signature, that.signature)
&& Objects.equals(unsignedPayload, that.unsignedPayload);
}
@Override
public int hashCode() {
return Objects.hash(sender, signature, unsignedPayload);
}
@Override
public String toString() {
return new StringJoiner(", ", SignedData.class.getSimpleName() + "[", "]")
.add("sender=" + sender)
.add("signature=" + signature)
.add("unsignedPayload=" + unsignedPayload)
.toString();
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.consensus.common.validator.blockbased;
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
import org.hyperledger.besu.consensus.common.validator.ValidatorVote;
import org.hyperledger.besu.datatypes.Address;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import com.google.common.collect.Maps;
/** Tracks the current list of validators and votes to add or drop validators. */
class VoteTally {
private final NavigableSet<Address> currentValidators;
private final Map<Address, Set<Address>> addVotesBySubject;
private final Map<Address, Set<Address>> removeVotesBySubject;
VoteTally(final Collection<Address> initialValidators) {
this(new TreeSet<>(initialValidators), new HashMap<>(), new HashMap<>());
}
private VoteTally(
final Collection<Address> initialValidators,
final Map<Address, Set<Address>> addVotesBySubject,
final Map<Address, Set<Address>> removeVotesBySubject) {
this.currentValidators = new TreeSet<>(initialValidators);
this.addVotesBySubject = addVotesBySubject;
this.removeVotesBySubject = removeVotesBySubject;
}
/**
* Add a vote to the current tally. The current validator list will be updated if this vote takes
* the tally past the required votes to approve the change.
*
* @param validatorVote The vote which was cast in a block header.
*/
void addVote(final ValidatorVote validatorVote) {
final Set<Address> addVotesForSubject =
addVotesBySubject.computeIfAbsent(validatorVote.getRecipient(), target -> new HashSet<>());
final Set<Address> removeVotesForSubject =
removeVotesBySubject.computeIfAbsent(
validatorVote.getRecipient(), target -> new HashSet<>());
if (validatorVote.isAuthVote()) {
addVotesForSubject.add(validatorVote.getProposer());
removeVotesForSubject.remove(validatorVote.getProposer());
} else {
removeVotesForSubject.add(validatorVote.getProposer());
addVotesForSubject.remove(validatorVote.getProposer());
}
final int validatorLimit = validatorLimit();
if (addVotesForSubject.size() >= validatorLimit) {
currentValidators.add(validatorVote.getRecipient());
discardOutstandingVotesFor(validatorVote.getRecipient());
}
if (removeVotesForSubject.size() >= validatorLimit) {
currentValidators.remove(validatorVote.getRecipient());
discardOutstandingVotesFor(validatorVote.getRecipient());
addVotesBySubject.values().forEach(votes -> votes.remove(validatorVote.getRecipient()));
removeVotesBySubject.values().forEach(votes -> votes.remove(validatorVote.getRecipient()));
}
}
private void discardOutstandingVotesFor(final Address subject) {
addVotesBySubject.remove(subject);
removeVotesBySubject.remove(subject);
}
Set<Address> getOutstandingAddVotesFor(final Address subject) {
return Optional.ofNullable(addVotesBySubject.get(subject)).orElse(Collections.emptySet());
}
Set<Address> getOutstandingRemoveVotesFor(final Address subject) {
return Optional.ofNullable(removeVotesBySubject.get(subject)).orElse(Collections.emptySet());
}
private int validatorLimit() {
return (currentValidators.size() / 2) + 1;
}
/**
* Reset the outstanding vote tallies as required at each epoch. The current validator list is
* unaffected.
*/
void discardOutstandingVotes() {
addVotesBySubject.clear();
removeVotesBySubject.clear();
}
/**
* AERE AIP-22 stage 3: after block H_id - 1 the validators become the sorted post-quantum ids of the
* same members, and votes still open on classical addresses are discarded (they name addresses the
* id space no longer contains). Anywhere else, and when the identity switch is not armed, nothing
* changes.
*
* @param blockNumber the block whose effects this tally now includes
*/
void translateToIds(final long blockNumber) {
final Collection<Address> translated =
AerePqIdentity.translateSetAfter(currentValidators, blockNumber);
if (translated != currentValidators) {
currentValidators.clear();
currentValidators.addAll(translated);
discardOutstandingVotes();
}
}
/**
* The validator addresses
*
* @return The collection of validators after the voting at the most recent block has been
* finalised.
*/
Collection<Address> getValidators() {
return currentValidators;
}
VoteTally copy() {
final Map<Address, Set<Address>> addVotesBySubject = Maps.newHashMap();
final Map<Address, Set<Address>> removeVotesBySubject = Maps.newHashMap();
this.addVotesBySubject.forEach(
(key, value) -> addVotesBySubject.put(key, new TreeSet<>(value)));
this.removeVotesBySubject.forEach(
(key, value) -> removeVotesBySubject.put(key, new TreeSet<>(value)));
return new VoteTally(
new TreeSet<>(this.currentValidators), addVotesBySubject, removeVotesBySubject);
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.consensus.common.validator.blockbased;
import org.hyperledger.besu.consensus.common.BlockInterface;
import org.hyperledger.besu.consensus.common.EpochManager;
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
import org.hyperledger.besu.consensus.common.validator.ValidatorVote;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import java.util.Collection;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides the logic to extract vote tally state from the blockchain and update it as blocks are
* added.
*/
class VoteTallyUpdater {
private static final Logger LOG = LoggerFactory.getLogger(VoteTallyUpdater.class);
private final EpochManager epochManager;
private final BlockInterface blockInterface;
VoteTallyUpdater(final EpochManager epochManager, final BlockInterface blockInterface) {
this.epochManager = epochManager;
this.blockInterface = blockInterface;
}
/**
* Create a new VoteTally based on the current blockchain state.
*
* @param blockchain the blockchain to load the current state from
* @return a VoteTally reflecting the state of the blockchain head
*/
VoteTally buildVoteTallyFromBlockchain(final Blockchain blockchain) {
final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber();
final long epochBlockNumber = epochManager.getLastEpochBlock(chainHeadBlockNumber);
LOG.info("Loading validator voting state starting from block {}", epochBlockNumber);
final BlockHeader epochBlock = blockchain.getBlockHeader(epochBlockNumber).get();
final Collection<Address> initialValidators = blockInterface.validatorsInBlock(epochBlock);
final VoteTally voteTally = new VoteTally(initialValidators);
for (long blockNumber = epochBlockNumber + 1;
blockNumber <= chainHeadBlockNumber;
blockNumber++) {
updateForBlock(blockchain.getBlockHeader(blockNumber).get(), voteTally);
}
return voteTally;
}
/**
* Update the vote tally to reflect changes caused by appending a new block to the chain.
*
* @param header the header of the block being added
* @param voteTally the vote tally to update
*/
void updateForBlock(final BlockHeader header, final VoteTally voteTally) {
if (epochManager.isEpochBlock(header.getNumber())) {
voteTally.discardOutstandingVotes();
} else {
final Optional<ValidatorVote> vote = blockInterface.extractVoteFromHeader(header);
vote.ifPresent(voteTally::addVote);
}
// AERE AIP-22 stage 3: after block H_id - 1 the set becomes the post-quantum ids of its members;
// asked first, so that disarmed (or at any other block) the tally sees no extra call at all
if (AerePqIdentity.translatesAfter(header.getNumber())) {
voteTally.translateToIds(header.getNumber());
}
}
}

View File

@ -0,0 +1,478 @@
/*
* Copyright contributors to Besu / AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.consensus.common.bft;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
import org.hyperledger.besu.crypto.SecureRandomProvider;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.function.LongFunction;
import java.util.stream.Stream;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.bouncycastle.crypto.digests.KeccakDigest;
import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
* AIP-21, SECOND HALF (2026-09-18): the finality window survives a restart.
*
* <p>The rule of the house: no proof without its negative pair. Real Falcon-512 keys, the real
* genesis-anchored registry, the real commit message, a real directory. What each test pins down:
*
* <ol>
* <li>{@link #theWindowSurvivesARestartAndOnlyCanonicalBlocksComeBack()} - the point of the whole
* thing, with its negative: a journal frame for a hash that is NOT canonical at its height
* never comes back, and neither does a height this node's chain does not hold.
* <li>{@link #aForgedSealInTheJournalNeverEntersTheWindow()} - THE load-bearing assertion, the one
* the build-time negative control plants against: three forgeries beside two genuine seals.
* <li>{@link #aTornTailOrAFlippedByteCostsOnlyWhatFollowsIt()} - a node that died mid-append, a
* flipped byte, garbage, a directory where a file should be: never an exception, and the good
* frames BEFORE the damage still come back.
* <li>{@link #aJournalFromAnotherChainOrOutsideTheWindowIsIgnored()}.
* <li>{@link #oldSegmentsAreDeletedSoTheFootprintStaysBounded()}.
* <li>{@link #theJournalIsOffWhenPersistenceIsOffOrThePropertySaysSo()}.
* <li>{@link #aRestoredWindowIsNotAppendedASecondTime()} - the journal does not grow by what it
* already holds every time the node restarts.
* <li>{@link #whatOneAppendCostsAgainstTheBlockInterval()} - the price on the consensus thread,
* as a number.
* </ol>
*
* <p>NOT MEASURED here: that a live validator answers {@code post-quantum} for a block heard before
* its restart. That is measured on testnet 28001 and recorded in the AIP.
*/
public class PqSealWindowPersistenceTest {
private static final long H = 1_000L;
private static final long ATTACH = 900L;
private static final int N = 7;
private static final long CHAIN_ID = 2_800L;
private static final long BLOCK_INTERVAL_MS = 523L;
private static final long HEAD = 5_000L;
@TempDir private Path tmp;
private Path dataDirectory;
private final List<FalconPrivateKeyParameters> privateKeys = new ArrayList<>();
private final Map<Long, BlockHeader> chain = new HashMap<>();
@BeforeEach
public void setUp() throws Exception {
dataDirectory = Files.createDirectories(tmp.resolve("besu-data"));
final KeccakDigest kd = new KeccakDigest(256);
final StringBuilder manifest =
new StringBuilder("{\"config\":{\"aereFalconRegistry\":{")
.append(PqV2Fixture.manifestHeader(N, CHAIN_ID, H));
for (int i = 0; i < N; i++) {
privateKeys.add(PqV2Fixture.privateKey(i));
final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i);
kd.update(anchoredRow, 0, anchoredRow.length);
manifest.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, H));
}
final byte[] anchoredHash = new byte[32];
kd.doFinal(anchoredHash, 0);
manifest
.append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x")
.append("0".repeat(64))
.append("\":\"0x")
.append(Bytes.wrap(anchoredHash).toUnprefixedHexString())
.append("\"}}}}");
final Path genesisPath = tmp.resolve("genesis-registry.json");
Files.writeString(genesisPath, manifest.toString());
System.setProperty("aere.falcon.genesis", genesisPath.toAbsolutePath().toString());
System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
System.clearProperty(PqSealWindowStore.PROPERTY_PERSIST);
resetFalconSingleton();
bindTheRegistryScheduleTheWayALiveNodeDoes(genesisPath);
PqSealCache.instance().disablePersistence();
PqSealCache.instance().clear();
PqAnchorProducer.useConfigForTesting(
new PqAnchorConfig(CHAIN_ID, H, Map.of(H, 0, H + 10L, 3), OptionalInt.empty(), false));
chain.clear();
}
/**
* The HISTORY door ({@code verifyAtHistoric}) refuses to resolve a key above the arming height
* unless a registry schedule is loaded (D2, 2026-08-06): it will not fall back to the head
* registry. A live node holds that schedule from its genesis ({@code config.pqRegistryHash}); the
* window restore judges heights BELOW the head, so it goes through that door, and so this fixture
* binds the schedule exactly the way a node does. Measured 2026-09-18: without this every restore
* here returned zero, with the refusal in the log.
*/
private void bindTheRegistryScheduleTheWayALiveNodeDoes(final Path genesisPath) throws Exception {
final PqRegistryHash.Registry held = PqRegistryHash.loadAuto(genesisPath);
String hash = PqRegistryHash.hashFor(held, CHAIN_ID);
if (!hash.startsWith("0x")) {
hash = "0x" + hash;
}
final Path schedule = tmp.resolve("genesis-schedule.json");
Files.writeString(
schedule,
"{\"config\":{\"pqRegistryHash\":[{\"block\":" + H + ",\"hash\":\"" + hash + "\"}]}}");
FalconSealSupport.instance()
.verifyRegistryBindingOrAbort(
0L, CHAIN_ID, PqRegistryHash.loadScheduleFromGenesis(schedule));
}
@AfterEach
public void tearDown() throws Exception {
System.clearProperty("aere.falcon.genesis");
System.clearProperty("aere.falcon.attachBlock");
System.clearProperty(PqSealWindowStore.PROPERTY_PERSIST);
PqSealCache.instance().disablePersistence();
PqSealCache.instance().clear();
PqAnchorProducer.useConfigForTesting(null);
resetFalconSingleton();
}
@Test
public void theWindowSurvivesARestartAndOnlyCanonicalBlocksComeBack() {
assertThat(FalconSealSupport.instance().genesisAnchored()).isTrue();
PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
// Ten heights heard live, three genuine seals each. A height reaches the journal when the NEXT
// one begins, so after hearing HEAD the journal holds HEAD-9 .. HEAD-1 and the head file HEAD.
for (long n = HEAD - 9L; n <= HEAD; n++) {
for (int i = 0; i < 3; i++) {
PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(i, header(n))));
}
}
// and one losing proposal at HEAD-4: heard, journaled, NOT canonical
final BlockHeader loser =
new BlockHeaderTestFixture().number(HEAD - 4L).extraData(Bytes.of(7, 7, 7)).buildHeader();
assertThat(loser.getHash()).isNotEqualTo(header(HEAD - 4L).getHash());
final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
assertThat(journal).isDirectory();
assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)).isEqualTo(9);
appendUnchecked(journal, loser, List.of(genuineSeal(0, loser)));
// THE RESTART: the in-memory window is gone
PqSealCache.instance().clear();
assertThat(PqSealCache.instance().sealCount(header(HEAD - 5L).getHash())).isZero();
// this node's chain holds every height except HEAD-7 (the negative: a height it cannot vouch for)
final LongFunction<Optional<Hash>> canonical =
n -> n == HEAD - 7L ? Optional.empty() : Optional.of(header(n).getHash());
final int restored =
PqSealCache.instance()
.restoreWindowFromDisk(HEAD, canonical, PqSignerRegistry.falconSealSupport());
assertThat(restored)
.describedAs("nine journaled heights, minus the one the chain does not hold, three seals each")
.isEqualTo(8 * 3);
for (long n = HEAD - 9L; n < HEAD; n++) {
assertThat(PqSealCache.instance().sealCount(header(n).getHash()))
.describedAs("height %d", n)
.isEqualTo(n == HEAD - 7L ? 0 : 3);
}
assertThat(PqSealCache.instance().sealCount(loser.getHash()))
.describedAs("a frame for a hash that is not canonical at its height never comes back")
.isZero();
assertThat(PqSealCache.instance().sealsFor(HEAD - 2L, header(HEAD - 2L).getHash()))
.extracting(FalconSeal::getValidatorIndex)
.containsExactly(0, 1, 2);
}
@Test
public void aForgedSealInTheJournalNeverEntersTheWindow() throws Exception {
final BlockHeader block = header(HEAD - 3L);
final FalconSeal genuine0 = genuineSeal(0, block);
final FalconSeal genuine1 = genuineSeal(1, block);
final Bytes32 anotherBlocksMessage =
PqAnchor.commitMessage(CHAIN_ID, block.getNumber() - 1L, Bytes32.leftPad(Bytes.of(9)));
final byte[] randomBytes = new byte[genuine0.getSignature().size()];
SecureRandomProvider.createSecureRandom().nextBytes(randomBytes);
final List<FalconSeal> all =
List.of(
genuine0,
genuine1,
new FalconSeal(2, Bytes.wrap(falconSign(privateKeys.get(2), anotherBlocksMessage))),
new FalconSeal(3, Bytes.wrap(randomBytes)),
new FalconSeal(4, genuine0.getSignature()));
final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
PqSealWindowStore.append(
journal, CHAIN_ID, PqSealWindowStore.heard(block.getNumber(), block.getHash().getBytes(), all));
PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
final int restored =
PqSealCache.instance()
.restoreWindowFromDisk(
HEAD, n -> Optional.of(header(n).getHash()), PqSignerRegistry.falconSealSupport());
assertThat(restored)
.describedAs(
"THE LOAD-BEARING ASSERTION. Five seals were in the frame and only the two genuine ones "
+ "may come out: a signature over another block's message, random bytes and a "
+ "genuine seal re-labelled under another index all die at the verifier. Take the "
+ "verifier out of restoreWindowFromDisk and this line goes red, which is what the "
+ "build-time negative control proves.")
.isEqualTo(2);
assertThat(PqSealCache.instance().sealsFor(block.getNumber(), block.getHash()))
.extracting(FalconSeal::getValidatorIndex)
.containsExactly(0, 1);
}
@Test
public void aTornTailOrAFlippedByteCostsOnlyWhatFollowsIt() throws Exception {
final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
// five heights in ONE segment (64 heights per segment; HEAD-5 .. HEAD-1 share 4992/64 = 78)
for (long n = HEAD - 5L; n < HEAD; n++) {
PqSealWindowStore.append(
journal,
CHAIN_ID,
PqSealWindowStore.heard(n, header(n).getHash().getBytes(), List.of(genuineSeal(0, header(n)))));
}
final Path segment = PqSealWindowStore.segmentFor(journal, HEAD - 1L);
assertThat(PqSealWindowStore.segmentFor(journal, HEAD - 5L)).isEqualTo(segment);
final byte[] whole = Files.readAllBytes(segment);
PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
// positive control first: the undamaged journal gives all five
assertThat(restoreAll()).isEqualTo(5);
// (a) torn tail: the node died in the middle of the fifth append
Files.write(segment, java.util.Arrays.copyOf(whole, whole.length - 40));
PqSealCache.instance().clear();
assertThat(restoreAll()).describedAs("the four whole frames before the torn one").isEqualTo(4);
// (b) one byte flipped inside the THIRD frame: the checksum catches it, frames one and two live
final int frame = whole.length / 5;
final byte[] flipped = whole.clone();
flipped[2 * frame + frame / 2] ^= 0x20;
Files.write(segment, flipped);
PqSealCache.instance().clear();
assertThat(restoreAll()).describedAs("the two frames before the damaged one").isEqualTo(2);
// (c) garbage, empty, a directory where the segment should be: nothing, and no exception
Files.write(segment, new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9});
PqSealCache.instance().clear();
assertThat(restoreAll()).isZero();
Files.write(segment, new byte[0]);
assertThat(restoreAll()).isZero();
Files.delete(segment);
Files.createDirectory(segment);
assertThatCode(this::restoreAll).doesNotThrowAnyException();
assertThat(restoreAll()).isZero();
}
@Test
public void aJournalFromAnotherChainOrOutsideTheWindowIsIgnored() throws Exception {
final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
final BlockHeader inWindow = header(HEAD - 10L);
final BlockHeader tooOld = header(HEAD - PqSealCache.instance().retentionBlocks() - 50L);
PqSealWindowStore.append(
journal,
CHAIN_ID + 1L,
PqSealWindowStore.heard(
inWindow.getNumber(), inWindow.getHash().getBytes(), List.of(genuineSeal(0, inWindow))));
PqSealWindowStore.append(
journal,
CHAIN_ID,
PqSealWindowStore.heard(
tooOld.getNumber(), tooOld.getHash().getBytes(), List.of(genuineSeal(0, tooOld))));
PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
assertThat(restoreAll())
.describedAs("another chain's frame and a height below the window: neither comes back")
.isZero();
// positive pair: the same height, this chain
PqSealWindowStore.append(
journal,
CHAIN_ID,
PqSealWindowStore.heard(
inWindow.getNumber(), inWindow.getHash().getBytes(), List.of(genuineSeal(0, inWindow))));
assertThat(restoreAll()).isEqualTo(1);
}
@Test
public void oldSegmentsAreDeletedSoTheFootprintStaysBounded() throws Exception {
PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
final long window = PqSealCache.instance().retentionBlocks();
final long heights = window + 5L * PqSealWindowStore.SEGMENT_BLOCKS;
final FalconSeal one = new FalconSeal(0, Bytes.wrap(new byte[650]));
for (long n = HEAD; n < HEAD + heights; n++) {
PqSealCache.instance().record(n, Hash.hash(Bytes.ofUnsignedLong(n)), List.of(one));
}
final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
final long highest = HEAD + heights - 1L;
try (Stream<Path> files = Files.list(journal)) {
final List<Path> segments = files.toList();
assertThat(segments.size())
.describedAs("segments kept: the window plus the one being written plus one of slack")
.isLessThanOrEqualTo((int) (window / PqSealWindowStore.SEGMENT_BLOCKS) + 2);
assertThat(segments)
.describedAs("no segment wholly below the window is left behind")
.doesNotContain(PqSealWindowStore.segmentFor(journal, highest - window - 2L * PqSealWindowStore.SEGMENT_BLOCKS));
assertThat(segments).contains(PqSealWindowStore.segmentFor(journal, highest - 1L));
}
}
@Test
public void theJournalIsOffWhenPersistenceIsOffOrThePropertySaysSo() throws Exception {
final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
// persistence never enabled: memory only, exactly as before this class existed
for (long n = HEAD; n < HEAD + 3L; n++) {
PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n))));
}
assertThat(journal).doesNotExist();
// the operator's switch
System.setProperty(PqSealWindowStore.PROPERTY_PERSIST, "false");
PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
for (long n = HEAD + 3L; n < HEAD + 6L; n++) {
PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n))));
}
assertThat(journal).doesNotExist();
assertThat(PqSealStore.fileIn(dataDirectory))
.describedAs("the head file, which liveness depends on, is NOT governed by this switch")
.exists();
// positive pair: the switch back on
System.clearProperty(PqSealWindowStore.PROPERTY_PERSIST);
PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
for (long n = HEAD + 6L; n < HEAD + 9L; n++) {
PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n))));
}
assertThat(journal).isDirectory();
assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)).isGreaterThanOrEqualTo(2);
}
@Test
public void aRestoredWindowIsNotAppendedASecondTime() {
PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
for (long n = HEAD - 4L; n <= HEAD; n++) {
PqSealCache.instance().record(n, header(n).getHash(), List.of(genuineSeal(0, header(n))));
}
final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID)).isEqualTo(4);
PqSealCache.instance().clear();
PqSealCache.instance()
.restoreFromDisk(HEAD, header(HEAD).getHash(), PqSignerRegistry.falconSealSupport());
assertThat(restoreAll()).isEqualTo(4);
// the chain moves on: exactly ONE new frame (the head, which was only in the head file)
PqSealCache.instance()
.record(HEAD + 1L, header(HEAD + 1L).getHash(), List.of(genuineSeal(0, header(HEAD + 1L))));
assertThat(PqSealWindowStore.frameCount(journal, CHAIN_ID))
.describedAs("four restored heights are not written again; the head is written once")
.isEqualTo(5);
}
@Test
public void whatOneAppendCostsAgainstTheBlockInterval() throws Exception {
final Path journal = PqSealWindowStore.directoryIn(dataDirectory);
final List<FalconSeal> ten = new ArrayList<>();
for (int i = 0; i < 10; i++) {
ten.add(new FalconSeal(i, Bytes.wrap(new byte[660])));
}
final int rounds = 200;
final long t0 = System.nanoTime();
for (int r = 0; r < rounds; r++) {
PqSealWindowStore.append(
journal, CHAIN_ID, PqSealWindowStore.heard(HEAD + r, Bytes32.leftPad(Bytes.of(r)), ten));
}
final double meanMs = (System.nanoTime() - t0) / 1_000_000.0 / rounds;
System.out.printf(
"AERE FEREASTRA-SIGILII: one append of 10 seals costs %.3f ms on average, %.2f%% of a %d ms block%n",
meanMs, 100.0 * meanMs / BLOCK_INTERVAL_MS, BLOCK_INTERVAL_MS);
assertThat(meanMs)
.describedAs("an append on the consensus thread must stay far below the block interval")
.isLessThan(BLOCK_INTERVAL_MS * 0.05);
}
/**
* THE SHARED VECTOR, literal for literal in the Nethermind-derived client's
* AereAip21SealWindowJournalProofTests: the two clients write the SAME frame payload. Chain 28001,
* block 5, hash 32 x 0xaa, one seal (index 1, signature 0x0102). The frame around it is
* length(4, big endian) || payload || crc32(payload)(4, big endian), java.util.zip.CRC32.
*/
@Test
public void theBytesOnDiskAreTheOnesTheOtherClientWrites() {
final Bytes hash = Bytes.fromHexString("0x" + "aa".repeat(32));
final byte[] payload =
PqSealWindowStore.encode(
28_001L, PqSealWindowStore.heard(5L, hash, List.of(new FalconSeal(1, Bytes.of(1, 2)))));
assertThat(Bytes.wrap(payload).toUnprefixedHexString())
.isEqualTo(
"f84094414552452d50512d5345414c57494e444f572d31826d6105a0"
+ "aa".repeat(32)
+ "c5c401820102");
final java.util.zip.CRC32 crc = new java.util.zip.CRC32();
crc.update("123456789".getBytes(java.nio.charset.StandardCharsets.US_ASCII));
assertThat(crc.getValue()).isEqualTo(0xCBF43926L);
}
private int restoreAll() {
return PqSealCache.instance()
.restoreWindowFromDisk(
HEAD, n -> Optional.of(header(n).getHash()), PqSignerRegistry.falconSealSupport());
}
private BlockHeader header(final long number) {
return chain.computeIfAbsent(
number, n -> new BlockHeaderTestFixture().number(n).buildHeader());
}
private FalconSeal genuineSeal(final int index, final BlockHeader block) {
final Bytes32 m =
PqAnchor.commitMessage(CHAIN_ID, block.getNumber(), block.getHash().getBytes());
return new FalconSeal(index, Bytes.wrap(falconSign(privateKeys.get(index), m)));
}
private static void appendUnchecked(
final Path journal, final BlockHeader block, final List<FalconSeal> seals) {
try {
PqSealWindowStore.append(
journal,
CHAIN_ID,
PqSealWindowStore.heard(block.getNumber(), block.getHash().getBytes(), seals));
} catch (final java.io.IOException e) {
throw new java.io.UncheckedIOException(e);
}
}
private static byte[] falconSign(final FalconPrivateKeyParameters key, final Bytes32 m) {
final FalconSigner signer = new FalconSigner();
signer.init(true, key);
return signer.generateSignature(m.toArray());
}
private static void resetFalconSingleton() throws Exception {
final Field f = FalconSealSupport.class.getDeclaredField("instance");
f.setAccessible(true);
f.set(null, null);
}
}

View File

@ -0,0 +1,136 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-bft-bounded-message-lists.patch.
*/
package org.hyperledger.besu.consensus.qbft.core.messagewrappers;
import org.hyperledger.besu.consensus.common.bft.messagewrappers.BftMessage;
import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
import org.hyperledger.besu.consensus.qbft.core.payload.PreparePayload;
import org.hyperledger.besu.consensus.qbft.core.payload.ProposalPayload;
import org.hyperledger.besu.consensus.qbft.core.payload.RoundChangePayload;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlock;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec;
import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPInput;
import java.util.List;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
/** The Proposal. */
public class Proposal extends BftMessage<ProposalPayload> {
private final List<SignedData<RoundChangePayload>> roundChanges;
private final List<SignedData<PreparePayload>> prepares;
/**
* Instantiates a new Proposal.
*
* @param payload the payload
* @param roundChanges the round changes
* @param prepares the prepares
*/
public Proposal(
final SignedData<ProposalPayload> payload,
final List<SignedData<RoundChangePayload>> roundChanges,
final List<SignedData<PreparePayload>> prepares) {
super(payload);
this.roundChanges = roundChanges;
this.prepares = prepares;
}
/**
* Gets round changes.
*
* @return the round changes
*/
public List<SignedData<RoundChangePayload>> getRoundChanges() {
return roundChanges;
}
/**
* Gets list of Prepare payload.
*
* @return the list of Prepare payload
*/
public List<SignedData<PreparePayload>> getPrepares() {
return prepares;
}
/**
* Gets block.
*
* @return the block
*/
public QbftBlock getBlock() {
return getPayload().getProposedBlock();
}
/**
* Gets block access list.
*
* @return the block access list
*/
public Optional<BlockAccessList> getBlockAccessList() {
return getPayload().getBlockAccessList();
}
@Override
public Bytes encode() {
final BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
rlpOut.startList();
getSignedPayload().writeTo(rlpOut);
rlpOut.startList();
rlpOut.writeList(roundChanges, SignedData::writeTo);
rlpOut.writeList(prepares, SignedData::writeTo);
rlpOut.endList();
rlpOut.endList();
return rlpOut.encoded();
}
/**
* Decode.
*
* @param data the data
* @param blockEncoder the qbft block encoder
* @return the proposal
*/
public static Proposal decode(final Bytes data, final QbftBlockCodec blockEncoder) {
final RLPInput rlpIn = RLP.input(data);
rlpIn.enterList();
final SignedData<ProposalPayload> payload =
readPayload(rlpIn, rlpInput -> ProposalPayload.readFrom(rlpInput, blockEncoder));
rlpIn.enterList();
final List<SignedData<RoundChangePayload>> roundChanges =
rlpIn.readList(r -> readPayload(r, RoundChangePayload::readFrom), MAX_LIST_ENTRIES);
final List<SignedData<PreparePayload>> prepares =
rlpIn.readList(r -> readPayload(r, PreparePayload::readFrom), MAX_LIST_ENTRIES);
rlpIn.leaveList();
rlpIn.leaveList();
return new Proposal(payload, roundChanges, prepares);
}
}

View File

@ -0,0 +1,168 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-bft-bounded-message-lists.patch.
*/
package org.hyperledger.besu.consensus.qbft.core.messagewrappers;
import org.hyperledger.besu.consensus.common.bft.messagewrappers.BftMessage;
import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
import org.hyperledger.besu.consensus.qbft.core.payload.PreparePayload;
import org.hyperledger.besu.consensus.qbft.core.payload.PreparedRoundMetadata;
import org.hyperledger.besu.consensus.qbft.core.payload.RoundChangePayload;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlock;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec;
import org.hyperledger.besu.ethereum.core.encoding.BlockAccessListDecoder;
import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPInput;
import java.util.List;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
/** The Round change payload message. */
public class RoundChange extends BftMessage<RoundChangePayload> {
private final Optional<QbftBlock> proposedBlock;
private final Optional<BlockAccessList> blockAccessList;
private final QbftBlockCodec blockEncoder;
private final List<SignedData<PreparePayload>> prepares;
/**
* Instantiates a new Round change.
*
* @param payload the payload
* @param proposedBlock the proposed block
* @param blockAccessList the block access list
* @param blockEncoder the qbft block encoder
* @param prepares the prepares
*/
public RoundChange(
final SignedData<RoundChangePayload> payload,
final Optional<QbftBlock> proposedBlock,
final Optional<BlockAccessList> blockAccessList,
final QbftBlockCodec blockEncoder,
final List<SignedData<PreparePayload>> prepares) {
super(payload);
this.proposedBlock = proposedBlock;
this.blockAccessList = blockAccessList;
this.blockEncoder = blockEncoder;
this.prepares = prepares;
}
/**
* Gets proposed block.
*
* @return the proposed block
*/
public Optional<QbftBlock> getProposedBlock() {
return proposedBlock;
}
/**
* Gets block access list.
*
* @return the block access list
*/
public Optional<BlockAccessList> getBlockAccessList() {
return blockAccessList;
}
/**
* Gets list of Prepare payload signed data.
*
* @return the prepares
*/
public List<SignedData<PreparePayload>> getPrepares() {
return prepares;
}
/**
* Gets prepared round metadata.
*
* @return the prepared round metadata
*/
public Optional<PreparedRoundMetadata> getPreparedRoundMetadata() {
return getPayload().getPreparedRoundMetadata();
}
/**
* Gets prepared round.
*
* @return the prepared round
*/
public Optional<Integer> getPreparedRound() {
return getPayload().getPreparedRoundMetadata().map(PreparedRoundMetadata::getPreparedRound);
}
@Override
public Bytes encode() {
final BytesValueRLPOutput rlpOut = new BytesValueRLPOutput();
rlpOut.startList();
getSignedPayload().writeTo(rlpOut);
proposedBlock.ifPresentOrElse(pb -> blockEncoder.writeTo(pb, rlpOut), rlpOut::writeEmptyList);
blockAccessList.ifPresentOrElse((bal) -> bal.writeTo(rlpOut), rlpOut::writeNull);
rlpOut.writeList(prepares, SignedData::writeTo);
rlpOut.endList();
return rlpOut.encoded();
}
/**
* Decode.
*
* @param data the data
* @param blockEncoder the qbft block encoder
* @return the round change
*/
public static RoundChange decode(final Bytes data, final QbftBlockCodec blockEncoder) {
final RLPInput rlpIn = RLP.input(data);
rlpIn.enterList();
final SignedData<RoundChangePayload> payload = readPayload(rlpIn, RoundChangePayload::readFrom);
final Optional<QbftBlock> block;
if (rlpIn.nextIsList() && rlpIn.nextSize() == 0) {
rlpIn.skipNext();
block = Optional.empty();
} else {
block = Optional.of(blockEncoder.readFrom(rlpIn));
}
final Optional<BlockAccessList> blockAccessList = readBlockAccessList(rlpIn);
final List<SignedData<PreparePayload>> prepares =
rlpIn.readList(r -> readPayload(r, PreparePayload::readFrom), MAX_LIST_ENTRIES);
rlpIn.leaveList();
return new RoundChange(payload, block, blockAccessList, blockEncoder, prepares);
}
private static Optional<BlockAccessList> readBlockAccessList(final RLPInput rlpIn) {
if (rlpIn.isEndOfCurrentList()) {
return Optional.empty();
}
if (!rlpIn.nextIsNull()) {
return Optional.of(BlockAccessListDecoder.decode(rlpIn));
}
rlpIn.skipNext();
return Optional.empty();
}
}

View File

@ -112,7 +112,7 @@ public class QbftRound {
// usual case). Each used to sign on its own, so at the parent of an anchor every Besu validator
// made two SLH-DSA signatures back to back on the consensus thread, the first before its Prepare
// and the second between "prepared" and its Commit. Measured on 2800 (2026-09-28, import times in
// ms on rpc2, scripts/masuratori/ritm-pe-propunator.mjs): the anchor-parent slot 1,036 ms against
// ms measured at a reading node of the network): the anchor-parent slot 1,036 ms against
// 599 for the other slots. The message is fixed by (block, commit hash), both fixed for the round
// once the proposal is accepted, so the second signing could only produce another valid seal over
// the same bytes. It now reuses the first, and the local and wire Commits carry the same seals

View File

@ -0,0 +1,124 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.consensus.qbft.core.statemachine;
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
import org.hyperledger.besu.consensus.qbft.core.network.QbftMessageTransmitter;
import org.hyperledger.besu.consensus.qbft.core.payload.MessageFactory;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCreator;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCreatorFactory;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockHeader;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockInterface;
import org.hyperledger.besu.consensus.qbft.core.types.QbftFinalState;
import org.hyperledger.besu.consensus.qbft.core.types.QbftMinedBlockObserver;
import org.hyperledger.besu.consensus.qbft.core.types.QbftProtocolSchedule;
import org.hyperledger.besu.consensus.qbft.core.validation.MessageValidatorFactory;
import org.hyperledger.besu.util.Subscribers;
/** The Qbft round factory. */
public class QbftRoundFactory {
private final QbftFinalState finalState;
private final QbftBlockCreatorFactory blockCreatorFactory;
private final QbftBlockInterface blockInterface;
private final QbftProtocolSchedule protocolSchedule;
private final Subscribers<QbftMinedBlockObserver> minedBlockObservers;
private final MessageValidatorFactory messageValidatorFactory;
private final MessageFactory messageFactory;
/**
* Instantiates a new Qbft round factory.
*
* @param finalState the final state
* @param blockInterface the block interface
* @param protocolSchedule the protocol schedule
* @param minedBlockObservers the mined block observers
* @param messageValidatorFactory the message validator factory
* @param messageFactory the message factory
*/
public QbftRoundFactory(
final QbftFinalState finalState,
final QbftBlockInterface blockInterface,
final QbftProtocolSchedule protocolSchedule,
final Subscribers<QbftMinedBlockObserver> minedBlockObservers,
final MessageValidatorFactory messageValidatorFactory,
final MessageFactory messageFactory) {
this.finalState = finalState;
this.blockCreatorFactory = finalState.getBlockCreatorFactory();
this.blockInterface = blockInterface;
this.protocolSchedule = protocolSchedule;
this.minedBlockObservers = minedBlockObservers;
this.messageValidatorFactory = messageValidatorFactory;
this.messageFactory = messageFactory;
}
/**
* Create new round qbft round.
*
* @param parentHeader the parent header
* @param round the round
* @return the qbft round
*/
public QbftRound createNewRound(final QbftBlockHeader parentHeader, final int round) {
long nextBlockHeight = parentHeader.getNumber() + 1;
final ConsensusRoundIdentifier roundIdentifier =
new ConsensusRoundIdentifier(nextBlockHeight, round);
final RoundState roundState =
new RoundState(
roundIdentifier,
finalState.getQuorum(),
messageValidatorFactory.createMessageValidator(roundIdentifier, parentHeader));
return createNewRoundWithState(parentHeader, roundState);
}
/**
* Create new Qbft round with state.
*
* @param parentHeader the parent header
* @param roundState the round state
* @return the qbft round
*/
public QbftRound createNewRoundWithState(
final QbftBlockHeader parentHeader, final RoundState roundState) {
final QbftBlockCreator blockCreator =
blockCreatorFactory.create(roundState.getRoundIdentifier().getRoundNumber());
// TODO(tmm): Why is this created everytime?!
final QbftMessageTransmitter messageTransmitter =
new QbftMessageTransmitter(messageFactory, finalState.getValidatorMulticaster());
return new QbftRound(
roundState,
blockCreator,
blockInterface,
protocolSchedule,
minedBlockObservers,
finalState.getNodeKey(),
AerePqIdentity.local(finalState.getLocalAddress(), parentHeader.getNumber() + 1L),
messageFactory,
messageTransmitter,
finalState.getRoundTimer(),
parentHeader);
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright contributors to Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Aere Network notice (Apache-2.0 section 4(b)): this file was added by Aere Network, 2026-09-28 (D-434), adapted from the tests of the
* ported upstream fix hyperledger/besu dd4101c9ec ("bound prepares/round-change lists at decode").
*/
package org.hyperledger.besu.consensus.qbft.core.messagewrappers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
import org.hyperledger.besu.consensus.common.bft.messagewrappers.BftMessage;
import org.hyperledger.besu.consensus.common.bft.payload.SignedData;
import org.hyperledger.besu.consensus.qbft.core.payload.PreparePayload;
import org.hyperledger.besu.consensus.qbft.core.payload.RoundChangePayload;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec;
import org.hyperledger.besu.cryptoservices.NodeKey;
import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.rlp.RLPException;
import java.util.Collections;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
/**
* AERE D-434: a RoundChange whose prepares list holds more than BftMessage.MAX_LIST_ENTRIES (512) entries is refused at DECODE, before
* any signature or author check; before the port any peer could make a node allocate and verify an arbitrarily long list. With ten
* validators an honest list never exceeds ten, so the bound never binds an honest network.
*/
class AereD434ListBoundsTest {
private static final QbftBlockCodec CODEC = Mockito.mock(QbftBlockCodec.class);
private static Bytes roundChangeWith(final int prepares) {
final NodeKey key = NodeKeyUtils.generate();
final RoundChangePayload rc = new RoundChangePayload(new ConsensusRoundIdentifier(1, 0), Optional.empty());
final SignedData<RoundChangePayload> signedRc = SignedData.create(rc, key.sign(Bytes32.wrap(rc.hashForSignature().getBytes())));
final PreparePayload p = new PreparePayload(new ConsensusRoundIdentifier(1, 0), Hash.ZERO);
final SignedData<PreparePayload> one = SignedData.create(p, key.sign(Bytes32.wrap(p.hashForSignature().getBytes())));
return new RoundChange(signedRc, Optional.empty(), Optional.empty(), CODEC, Collections.nCopies(prepares, one)).encode();
}
@Test
void aPreparesListAboveTheBoundIsRefusedAtDecode() {
final Bytes oversized = roundChangeWith(BftMessage.MAX_LIST_ENTRIES + 1);
assertThatThrownBy(() -> RoundChange.decode(oversized, CODEC))
.isInstanceOf(RLPException.class)
.hasMessageContaining("exceeds the maximum permitted size");
}
@Test
void aPreparesListAtTheBoundStillDecodes() {
// positive control: the fixture decodes, and the bound is inclusive
assertThat(RoundChange.decode(roundChangeWith(BftMessage.MAX_LIST_ENTRIES), CODEC).getPrepares()).hasSize(BftMessage.MAX_LIST_ENTRIES);
}
}

View File

@ -0,0 +1,196 @@
/*
* Copyright contributors to Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.consensus.qbft.adaptor;
import org.hyperledger.besu.consensus.common.bft.AerePqIdentity;
import org.hyperledger.besu.consensus.common.bft.BftHelpers;
import org.hyperledger.besu.consensus.common.bft.BlockTimer;
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
import org.hyperledger.besu.consensus.common.bft.RoundTimer;
import org.hyperledger.besu.consensus.common.bft.blockcreation.ProposerSelector;
import org.hyperledger.besu.consensus.common.bft.network.ValidatorMulticaster;
import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCreatorFactory;
import org.hyperledger.besu.consensus.qbft.core.types.QbftFinalState;
import org.hyperledger.besu.cryptoservices.NodeKey;
import org.hyperledger.besu.datatypes.Address;
import java.time.Clock;
import java.util.Collection;
/** Besu implementation of QbftFinalState for maintaining the state of a QBFT network. */
public class QbftFinalStateImpl implements QbftFinalState {
private final ValidatorProvider validatorProvider;
private final NodeKey nodeKey;
private final Address localAddress;
private final ProposerSelector proposerSelector;
private final ValidatorMulticaster validatorMulticaster;
private final RoundTimer roundTimer;
private final BlockTimer blockTimer;
private final QbftBlockCreatorFactory blockCreatorFactory;
private final Clock clock;
/**
* Constructs a new QBFT final state.
*
* @param validatorProvider the validator provider
* @param nodeKey the node key
* @param localAddress the local address
* @param proposerSelector the proposer selector
* @param validatorMulticaster the validator multicaster
* @param roundTimer the round timer
* @param blockTimer the block timer
* @param blockCreatorFactory the block creator factory
* @param clock the clock
*/
public QbftFinalStateImpl(
final ValidatorProvider validatorProvider,
final NodeKey nodeKey,
final Address localAddress,
final ProposerSelector proposerSelector,
final ValidatorMulticaster validatorMulticaster,
final RoundTimer roundTimer,
final BlockTimer blockTimer,
final QbftBlockCreatorFactory blockCreatorFactory,
final Clock clock) {
this.validatorProvider = validatorProvider;
this.nodeKey = nodeKey;
this.localAddress = localAddress;
this.proposerSelector = proposerSelector;
this.validatorMulticaster = validatorMulticaster;
this.roundTimer = roundTimer;
this.blockTimer = blockTimer;
this.blockCreatorFactory = blockCreatorFactory;
this.clock = clock;
}
/**
* Gets validators.
*
* @return the validators
*/
@Override
public Collection<Address> getValidators() {
return validatorProvider.getValidatorsAtHead();
}
/**
* Gets the validator multicaster.
*
* @return the validator multicaster
*/
@Override
public ValidatorMulticaster getValidatorMulticaster() {
return validatorMulticaster;
}
/**
* Gets node key.
*
* @return the node key
*/
@Override
public NodeKey getNodeKey() {
return nodeKey;
}
/**
* Gets local address.
*
* @return the local address
*/
@Override
public Address getLocalAddress() {
return localAddress;
}
/**
* Is local node validator.
*
* @return the boolean
*/
@Override
public boolean isLocalNodeValidator() {
return getValidators().stream().anyMatch(a -> AerePqIdentity.isLocal(a, localAddress));
}
/**
* Gets round timer.
*
* @return the round timer
*/
@Override
public RoundTimer getRoundTimer() {
return roundTimer;
}
/**
* Gets block creator factory.
*
* @return the block creator factory
*/
@Override
public QbftBlockCreatorFactory getBlockCreatorFactory() {
return blockCreatorFactory;
}
@Override
public int getQuorum() {
return BftHelpers.calculateRequiredValidatorQuorum(getValidators().size());
}
@Override
public BlockTimer getBlockTimer() {
return blockTimer;
}
/**
* Is local node proposer for round.
*
* @param roundIdentifier the round identifier
* @return the boolean
*/
@Override
public boolean isLocalNodeProposerForRound(final ConsensusRoundIdentifier roundIdentifier) {
return getProposerForRound(roundIdentifier)
.equals(AerePqIdentity.local(localAddress, roundIdentifier.getSequenceNumber()));
}
/**
* Gets proposer for round.
*
* @param roundIdentifier the round identifier
* @return the proposer for round
*/
public Address getProposerForRound(final ConsensusRoundIdentifier roundIdentifier) {
return proposerSelector.selectProposerForRound(roundIdentifier);
}
/**
* Gets clock.
*
* @return the clock
*/
@Override
public Clock getClock() {
return clock;
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0007-aere-pq-finality.patch.
*/
package org.hyperledger.besu.consensus.qbft.jsonrpc;
import org.hyperledger.besu.config.BftConfigOptions;
import org.hyperledger.besu.consensus.common.BlockInterface;
import org.hyperledger.besu.consensus.common.bft.BftContext;
import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftDiscardValidatorVote;
import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetPendingVotes;
import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.AereGetPqFinality;
import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetRequestTimeoutSeconds;
import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetSignerMetrics;
import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetValidatorsByBlockHash;
import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftGetValidatorsByBlockNumber;
import org.hyperledger.besu.consensus.qbft.jsonrpc.methods.QbftProposeValidatorVote;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.methods.ApiGroupJsonRpcMethods;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.core.MiningConfiguration;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import java.util.Map;
/** The Qbft json rpc methods. */
public class QbftJsonRpcMethods extends ApiGroupJsonRpcMethods {
private final ProtocolContext context;
private final ValidatorProvider readOnlyValidatorProvider;
private final ProtocolSchedule protocolSchedule;
private final MiningConfiguration miningConfiguration;
private final BftConfigOptions bftConfig;
/**
* Instantiates a new Qbft json rpc methods.
*
* @param context the protocol context
* @param protocolSchedule the protocol schedule
* @param miningConfiguration the mining parameters
* @param readOnlyValidatorProvider the read only validator provider
* @param bftConfig the BFT config options, containing QBFT-specific settings
*/
public QbftJsonRpcMethods(
final ProtocolContext context,
final ProtocolSchedule protocolSchedule,
final MiningConfiguration miningConfiguration,
final ValidatorProvider readOnlyValidatorProvider,
final BftConfigOptions bftConfig) {
this.context = context;
this.readOnlyValidatorProvider = readOnlyValidatorProvider;
this.protocolSchedule = protocolSchedule;
this.miningConfiguration = miningConfiguration;
this.bftConfig = bftConfig;
}
@Override
protected String getApiGroup() {
return RpcApis.QBFT.name();
}
@Override
protected Map<String, JsonRpcMethod> create() {
final BlockchainQueries blockchainQueries =
new BlockchainQueries(
protocolSchedule,
context.getBlockchain(),
context.getWorldStateArchive(),
miningConfiguration);
final BftContext bftContext = context.getConsensusContext(BftContext.class);
final BlockInterface blockInterface = bftContext.getBlockInterface();
final ValidatorProvider validatorProvider = bftContext.getValidatorProvider();
Map<String, JsonRpcMethod> methods =
mapOf(
new QbftProposeValidatorVote(validatorProvider),
new QbftGetValidatorsByBlockNumber(blockchainQueries, readOnlyValidatorProvider),
new QbftDiscardValidatorVote(validatorProvider),
new QbftGetValidatorsByBlockHash(context.getBlockchain(), readOnlyValidatorProvider),
new QbftGetSignerMetrics(readOnlyValidatorProvider, blockInterface, blockchainQueries),
new QbftGetPendingVotes(validatorProvider),
new QbftGetRequestTimeoutSeconds(bftConfig),
// AIP-21 (AERE): per-block post-quantum finality, non-consensus, verified against the anchored registry
new AereGetPqFinality(blockchainQueries, readOnlyValidatorProvider));
return methods;
}
}

View File

@ -0,0 +1,254 @@
/*
* Copyright contributors to Besu / AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.consensus.qbft.jsonrpc.methods;
import org.hyperledger.besu.consensus.common.bft.BftHelpers;
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
import org.hyperledger.besu.consensus.common.bft.PqSealCache;
import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.AbstractBlockParameterMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameter;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeSet;
import org.apache.tuweni.bytes.Bytes;
/**
* AIP-21 (AERE, 2026-09-18), first half: the post-quantum finality of ONE block, verifiable per block,
* without growing the header.
*
* <p>WHAT IT ANSWERS. Every block of chain 2800 is committed with a Falcon-512 seal on each Commit
* message (enforced since H1, 2026-09-05), and every validator keeps the seals it heard for recent
* blocks in {@link PqSealCache}. Those seals never reach the header (only every 128th block carries a
* certificate, over its parent), so until now the immediate post-quantum finality of a block was true
* and NOT observable. This method exposes it: for a block it returns the seals heard, re-verifies each
* one right now against the anchored registry over the exact message the validators signed, and gives
* the verdict "verified distinct signers &gt;= the chain's commit quorum".
*
* <p>WHAT IT DOES NOT CLAIM. Seals travel only on Commit messages, which validators send to
* validators: a reader node hears none and answers {@code finality: "unavailable"} honestly. The
* retention window is the cache's (system property {@code aere.pq.finality.window}, blocks; default
* 256), so an older block answers "unavailable" too, and the permanent record for it is the anchor
* certificate. Hybrid extras (SLH-DSA seals on anchor parents) are listed as heard but NOT verified
* here ({@code verified: null}); their verification is the anchor certificate's job. Below the anchor
* form (never on a live chain today) the signed message is the round-specific commit digest, which
* cannot be rebuilt from the header, so the method says so instead of guessing.
*
* <p>NON-CONSENSUS. Nothing here changes what a block is; a node with or without this method imports
* the same chain. That is why it needs no activation height.
*/
public class AereGetPqFinality extends AbstractBlockParameterMethod {
/** Verifies one Falcon seal over the message signed for a block; injectable for tests. */
@FunctionalInterface
public interface SealVerifier {
/**
* Verify.
*
* @param blockNumber the height
* @param validatorIndex the signer index in the anchored registry
* @param message the message that was signed
* @param signature the Falcon signature
* @return true iff it verifies against the key bound at that height
*/
boolean verify(long blockNumber, int validatorIndex, Bytes message, Bytes signature);
}
/** Rebuilds the message the seals of a block signed, when that is possible from the header. */
@FunctionalInterface
public interface MessageBuilder {
/**
* The signed message.
*
* @param blockNumber the height
* @param onchainHash the round-independent on-chain hash
* @return the message, or empty when the form at that height cannot be rebuilt from the header
*/
Optional<Bytes> messageFor(long blockNumber, Hash onchainHash);
}
private final ValidatorProvider validatorProvider;
private final PqSealCache cache;
private final SealVerifier verifier;
private final MessageBuilder messageBuilder;
/**
* Production wiring: the process-wide seal cache, the anchored Falcon registry at the block's
* height, and the anchor-form commit message.
*
* @param blockchainQueries the blockchain queries
* @param validatorProvider the read-only validator provider
*/
public AereGetPqFinality(
final BlockchainQueries blockchainQueries, final ValidatorProvider validatorProvider) {
this(
blockchainQueries,
validatorProvider,
PqSealCache.instance(),
(n, i, m, s) -> PqSignerRegistry.falconSealSupport().verifyAtHistoric(n, i, m, s),
(n, h) ->
PqAnchorProducer.sealMessageIsAnchorForm(n)
? Optional.of(PqAnchorProducer.commitSealMessage(n, h.getBytes(), Hash.ZERO))
: Optional.empty());
}
/**
* Full wiring, for tests.
*
* @param blockchainQueries the blockchain queries
* @param validatorProvider the validator provider
* @param cache the seal cache to read
* @param verifier the seal verifier
* @param messageBuilder the signed-message builder
*/
public AereGetPqFinality(
final BlockchainQueries blockchainQueries,
final ValidatorProvider validatorProvider,
final PqSealCache cache,
final SealVerifier verifier,
final MessageBuilder messageBuilder) {
super(blockchainQueries);
this.validatorProvider = validatorProvider;
this.cache = cache;
this.verifier = verifier;
this.messageBuilder = messageBuilder;
}
@Override
public String getName() {
return RpcMethod.AERE_GET_PQ_FINALITY.getMethodName();
}
@Override
protected BlockParameter blockParameter(final JsonRpcRequestContext request) {
try {
return request.getOptionalParameter(0, BlockParameter.class).orElse(BlockParameter.LATEST);
} catch (final JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid block parameter (index 0)", RpcErrorType.INVALID_BLOCK_PARAMS, e);
}
}
@Override
protected Object pendingResult(final JsonRpcRequestContext request) {
return describe(getBlockchainQueries().headBlockHeader());
}
@Override
protected Object resultByBlockNumber(final JsonRpcRequestContext request, final long blockNumber) {
return getBlockchainQueries().getBlockHeaderByNumber(blockNumber).map(this::describe).orElse(null);
}
/**
* The finality record of one block.
*
* @param header the block's header
* @return the JSON-shaped record
*/
public Map<String, Object> describe(final BlockHeader header) {
final long number = header.getNumber();
final Hash hash = header.getHash();
final int validators = validatorProvider.getValidatorsForBlock(header).size();
final int quorum = validators == 0 ? 0 : BftHelpers.calculateRequiredValidatorQuorum(validators);
final Optional<Bytes> message = messageBuilder.messageFor(number, hash);
final List<FalconSeal> heard = cache.sealsFor(number, hash);
final List<SchemeSeal> extras = cache.extrasFor(number, hash);
final List<Map<String, Object>> seals = new ArrayList<>();
final TreeSet<Integer> verifiedSigners = new TreeSet<>();
for (final FalconSeal seal : heard) {
final boolean ok =
message.isPresent()
&& verifier.verify(number, seal.getValidatorIndex(), message.get(), seal.getSignature());
if (ok) {
verifiedSigners.add(seal.getValidatorIndex());
}
final Map<String, Object> s = new LinkedHashMap<>();
s.put("scheme", "falcon-512");
s.put("index", seal.getValidatorIndex());
s.put("verified", ok);
s.put("signature", seal.getSignature().toHexString());
seals.add(s);
}
final List<Map<String, Object>> hybrid = new ArrayList<>();
for (final SchemeSeal seal : extras) {
final Map<String, Object> s = new LinkedHashMap<>();
s.put(
"scheme",
SealSchemes.byWireId(seal.getSchemeWireId()).map(x -> x.id()).orElse("wire-" + (seal.getSchemeWireId() & 0xff)));
s.put("index", seal.getValidatorIndex());
s.put("verified", null);
s.put("signature", seal.getSignature().toHexString());
hybrid.add(s);
}
final String finality;
final String note;
if (message.isEmpty()) {
finality = "unverifiable-form";
note =
"below the anchor form the signed message is the round-specific commit digest, which this node cannot rebuild from the header; nothing is guessed";
} else if (heard.isEmpty()) {
finality = "unavailable";
note =
"no seal heard for this block: seals travel on Commit messages between validators, so a reader node hears none, and a validator keeps only the last "
+ cache.retentionBlocks()
+ " blocks; the permanent record for older blocks is the anchor certificate";
} else if (verifiedSigners.size() >= quorum && quorum > 0) {
finality = "post-quantum";
note = "at least the commit quorum of distinct validators sealed this block hash with Falcon-512 and every counted seal verifies now against the anchored registry";
} else if (!verifiedSigners.isEmpty()) {
finality = "partial";
note = "fewer verified distinct signers than the commit quorum: this node did not hear enough seals, or some it heard no longer verify";
} else {
finality = "none";
note = "seals were heard but none verifies against the anchored registry at this height";
}
final Map<String, Object> out = new LinkedHashMap<>();
out.put("blockNumber", number);
out.put("blockHash", hash.getBytes().toHexString());
out.put("validators", validators);
out.put("quorum", quorum);
out.put("sealForm", message.isPresent() ? "anchor" : "commit-digest");
out.put("signedMessage", message.map(Bytes::toHexString).orElse(null));
out.put("falconSealsHeard", heard.size());
out.put("falconSealsVerified", verifiedSigners.size());
out.put("verifiedIndexes", new ArrayList<>(verifiedSigners));
out.put("falconSeals", seals);
out.put("hybridSeals", hybrid);
out.put("finality", finality);
out.put("retentionBlocks", cache.retentionBlocks());
out.put("note", note);
return out;
}
}

View File

@ -0,0 +1,205 @@
/*
* Copyright contributors to Besu / AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.consensus.qbft.jsonrpc.methods;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
import org.hyperledger.besu.consensus.common.bft.PqSealCache;
import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* AIP-21: the finality record is a MEASUREMENT of the seals heard, re-verified, against the chain's
* commit quorum; every claim it can make has its negative pair here (too few signers, none verifying,
* nothing heard, a form that cannot be rebuilt), and the verifier is consulted with the exact message
* and signature, so a stub that verifies nothing turns the verdict from post-quantum to none.
*/
@ExtendWith(MockitoExtension.class)
public class AereGetPqFinalityTest {
private static final long NUMBER = 19_018_096L;
private static final Hash HASH = Hash.fromHexString("0xf776f80e1710dc0e0f1927958dd589b98fc35ce8b9854e4fdb12320459fbb3f1");
private static final Bytes MESSAGE = Bytes32.fromHexStringLenient("0x1122");
@Mock private BlockchainQueries blockchainQueries;
@Mock private BlockHeader header;
@Mock private ValidatorProvider validatorProvider;
private final PqSealCache cache = PqSealCache.instance();
private final List<Object[]> verifierCalls = new ArrayList<>();
@BeforeEach
public void setUp() {
cache.clear();
// lenient: the name and unknown-block cases never touch the header, and strict stubs would call that a defect
lenient().when(header.getNumber()).thenReturn(NUMBER);
lenient().when(header.getHash()).thenReturn(HASH);
}
@AfterEach
public void tearDown() {
cache.clear();
}
private static List<Address> validators(final int n) {
final List<Address> out = new ArrayList<>();
for (int i = 0; i < n; i++) {
out.add(Address.fromHexString(String.format("0x%040x", 0xA000 + i)));
}
return out;
}
private void heard(final int count) {
final List<FalconSeal> seals = new ArrayList<>();
for (int i = 0; i < count; i++) {
seals.add(new FalconSeal(i, Bytes.of(0x29, i, 7, 7, 7)));
}
cache.record(NUMBER, HASH, seals);
}
private AereGetPqFinality method(final int verifyBelowIndex, final Optional<Bytes> message) {
return new AereGetPqFinality(
blockchainQueries,
validatorProvider,
cache,
(n, i, m, s) -> {
verifierCalls.add(new Object[] {n, i, m, s});
return i < verifyBelowIndex;
},
(n, h) -> message);
}
@SuppressWarnings("unchecked")
private Map<String, Object> call(final AereGetPqFinality method, final Object... params) {
final JsonRpcRequestContext request =
new JsonRpcRequestContext(new JsonRpcRequest("2.0", method.getName(), params));
final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request);
return (Map<String, Object>) response.getResult();
}
@Test
public void nameIsTheAipMethod() {
assertThat(method(7, Optional.of(MESSAGE)).getName()).isEqualTo("aere_getPqFinality");
}
@Test
public void quorumOfVerifiedDistinctSignersIsPostQuantumFinality() {
when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7));
when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header));
heard(7);
final Map<String, Object> r = call(method(5, Optional.of(MESSAGE)), "0x" + Long.toHexString(NUMBER));
assertThat(r.get("finality")).isEqualTo("post-quantum");
assertThat(r.get("quorum")).isEqualTo(5);
assertThat(r.get("falconSealsHeard")).isEqualTo(7);
assertThat(r.get("falconSealsVerified")).isEqualTo(5);
assertThat(r.get("sealForm")).isEqualTo("anchor");
assertThat(r.get("signedMessage")).isEqualTo(MESSAGE.toHexString());
// the verifier saw exactly the message and each seal's own signature
assertThat(verifierCalls).hasSize(7);
assertThat(verifierCalls.get(3)[1]).isEqualTo(3);
assertThat(verifierCalls.get(3)[2]).isEqualTo(MESSAGE);
assertThat(verifierCalls.get(3)[3]).isEqualTo(Bytes.of(0x29, 3, 7, 7, 7));
}
@Test
public void oneSignerShortOfQuorumIsPartial() {
when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7));
when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header));
heard(7);
final Map<String, Object> r = call(method(4, Optional.of(MESSAGE)), "0x" + Long.toHexString(NUMBER));
assertThat(r.get("finality")).isEqualTo("partial");
assertThat(r.get("falconSealsVerified")).isEqualTo(4);
}
@Test
public void negativeControlNothingVerifiesIsNone() {
when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7));
when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header));
heard(7);
final Map<String, Object> r = call(method(0, Optional.of(MESSAGE)), "0x" + Long.toHexString(NUMBER));
assertThat(r.get("finality")).isEqualTo("none");
assertThat(r.get("falconSealsVerified")).isEqualTo(0);
assertThat(r.get("falconSealsHeard")).isEqualTo(7);
}
@Test
public void nothingHeardIsUnavailableNotFalse() {
when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7));
when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header));
final Map<String, Object> r = call(method(7, Optional.of(MESSAGE)), "0x" + Long.toHexString(NUMBER));
assertThat(r.get("finality")).isEqualTo("unavailable");
assertThat(r.get("falconSealsHeard")).isEqualTo(0);
assertThat(verifierCalls).isEmpty();
}
@Test
public void aFormThatCannotBeRebuiltIsSaidNotGuessed() {
when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(7));
when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header));
heard(7);
final Map<String, Object> r = call(method(7, Optional.empty()), "0x" + Long.toHexString(NUMBER));
assertThat(r.get("finality")).isEqualTo("unverifiable-form");
assertThat(r.get("sealForm")).isEqualTo("commit-digest");
assertThat(r.get("falconSealsVerified")).isEqualTo(0);
assertThat(verifierCalls).isEmpty();
}
@Test
public void latestIsTheDefaultBlock() {
when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators(10));
// "latest" resolves through the head NUMBER, then the header by number (AbstractBlockParameterMethod)
when(blockchainQueries.headBlockNumber()).thenReturn(NUMBER);
when(blockchainQueries.getBlockHeaderByNumber(NUMBER)).thenReturn(Optional.of(header));
heard(10);
final Map<String, Object> r = call(method(10, Optional.of(MESSAGE)));
assertThat(r.get("quorum")).isEqualTo(7);
assertThat(r.get("finality")).isEqualTo("post-quantum");
assertThat(r.get("blockNumber")).isEqualTo(NUMBER);
}
@Test
public void unknownBlockIsNull() {
when(blockchainQueries.getBlockHeaderByNumber(5L)).thenReturn(Optional.empty());
final JsonRpcRequestContext request =
new JsonRpcRequestContext(new JsonRpcRequest("2.0", "aere_getPqFinality", new Object[] {"0x5"}));
final JsonRpcSuccessResponse response =
(JsonRpcSuccessResponse) method(7, Optional.of(MESSAGE)).response(request);
assertThat(response.getResult()).isNull();
}
}

View File

@ -0,0 +1,274 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0001-aere-pqc-precompiles-mainnet.patch.
*/
package org.hyperledger.besu.datatypes;
import static com.google.common.base.Preconditions.checkArgument;
import static org.hyperledger.besu.crypto.Hash.keccak256;
import org.hyperledger.besu.crypto.SECPPublicKey;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPException;
import org.hyperledger.besu.ethereum.rlp.RLPInput;
import java.util.concurrent.ExecutionException;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
/** A 160-bits account address. */
public class Address extends BytesHolder {
/** The constant SIZE. */
public static final int SIZE = 20;
/** Specific addresses of the "precompiled" contracts. */
public static final Address ECREC = Address.precompiled(0x01);
/** The constant SHA256. */
public static final Address SHA256 = Address.precompiled(0x02);
/** The constant RIPEMD160. */
public static final Address RIPEMD160 = Address.precompiled(0x03);
/** The constant ID. */
public static final Address ID = Address.precompiled(0x04);
/** The constant MODEXP. */
public static final Address MODEXP = Address.precompiled(0x05);
/** The constant ALTBN128_ADD. */
public static final Address ALTBN128_ADD = Address.precompiled(0x06);
/** The constant ALTBN128_MUL. */
public static final Address ALTBN128_MUL = Address.precompiled(0x07);
/** The constant ALTBN128_PAIRING. */
public static final Address ALTBN128_PAIRING = Address.precompiled(0x08);
/** The constant BLAKE2B_F_COMPRESSION. */
public static final Address BLAKE2B_F_COMPRESSION = Address.precompiled(0x09);
/** The constant KZG_POINT_EVAL aka POINT_EVALUATION_PRECOMPILE_ADDRESS. */
public static final Address KZG_POINT_EVAL = Address.precompiled(0xA);
/** The constant BLS12_G1ADD. */
public static final Address BLS12_G1ADD = Address.precompiled(0xB);
/** The constant BLS12_G1MULTIEXP. */
public static final Address BLS12_G1MULTIEXP = Address.precompiled(0xC);
/** The constant BLS12_G2ADD. */
public static final Address BLS12_G2ADD = Address.precompiled(0xD);
/** The constant BLS12_G2MULTIEXP. */
public static final Address BLS12_G2MULTIEXP = Address.precompiled(0xE);
/** The constant BLS12_PAIRING. */
public static final Address BLS12_PAIRING = Address.precompiled(0xF);
/** The constant BLS12_MAP_FP_TO_G1. */
public static final Address BLS12_MAP_FP_TO_G1 = Address.precompiled(0x10);
/** The constant BLS12_MAP_FP2_TO_G2. */
public static final Address BLS12_MAP_FP2_TO_G2 = Address.precompiled(0x11);
/** Precompile address for P256_VERIFY. */
public static final Address P256_VERIFY = Address.precompiled(0x0100);
/** AERE PQC precompile: Falcon-512 verify. */
public static final Address AERE_FALCON512 = Address.fromHexString("0x0000000000000000000000000000000000000ae1");
/** AERE PQC precompile: Falcon-1024 verify. */
public static final Address AERE_FALCON1024 = Address.fromHexString("0x0000000000000000000000000000000000000ae2");
/** AERE PQC precompile: ML-DSA-44 (FIPS 204) verify. */
public static final Address AERE_MLDSA44 = Address.fromHexString("0x0000000000000000000000000000000000000ae3");
/** AERE PQC precompile: SLH-DSA-SHA2-128s (FIPS 205) verify. */
public static final Address AERE_SLHDSA128S = Address.fromHexString("0x0000000000000000000000000000000000000ae4");
/** AERE PQC precompile: SHAKE256 XOF (FIPS 202). */
public static final Address AERE_SHAKE256 = Address.fromHexString("0x0000000000000000000000000000000000000ae5");
/** The constant ZERO. */
public static final Address ZERO = Address.fromHexString("0x0");
static LoadingCache<Address, Hash> hashCache =
CacheBuilder.newBuilder()
.maximumSize(4000)
// .weakKeys() // unless we "intern" all addresses we cannot use weak or soft keys.
.build(
new CacheLoader<>() {
@Override
public Hash load(final Address key) {
return Hash.hash(key.getBytes());
}
});
/**
* Instantiates a new Address.
*
* @param bytes the bytes
*/
protected Address(final Bytes bytes) {
super(bytes);
}
/**
* Wrap address.
*
* @param value the value
* @return the address
*/
public static Address wrap(final Bytes value) {
checkArgument(
value.size() == SIZE,
"An account address must be %s bytes long, got %s",
SIZE,
value.size());
return new Address(value);
}
/**
* Creates an address from the given RLP-encoded input.
*
* @param input The input to read from
* @return the input's corresponding address
*/
public static Address readFrom(final RLPInput input) {
final Bytes bytes = input.readBytes();
if (bytes.size() != SIZE) {
throw new RLPException(
String.format("Address unexpected size of %s (needs %s)", bytes.size(), SIZE));
}
return Address.wrap(bytes);
}
/**
* Extracts an address from a ECDSARECOVER result hash.
*
* @param hash A hash that has been obtained through hashing the return of the <code>
* ECDSARECOVER </code> function from Appendix F (Signing Transactions) of the Ethereum
* Yellow Paper.
* @return The ethereum address from the provided hash.
*/
public static Address extract(final Bytes32 hash) {
return wrap(hash.slice(12, 20));
}
/**
* Extract address.
*
* @param publicKey the public key
* @return the address
*/
public static Address extract(final SECPPublicKey publicKey) {
return Address.extract(keccak256(publicKey.getEncodedBytes()));
}
/**
* Parse a hexadecimal string representing an account address.
*
* @param str A hexadecimal string (with or without the leading '0x') representing a valid account
* address.
* @return The parsed address: {@code null} if the provided string is {@code null}.
* @throws IllegalArgumentException if the string is either not hexadecimal, or not the valid
* representation of an address.
*/
@JsonCreator
public static Address fromHexString(final String str) {
if (str == null) return null;
return wrap(Bytes.fromHexStringLenient(str, SIZE));
}
/**
* Parse a hexadecimal string representing an account address.
*
* @param str A hexadecimal string representing a valid account address (strictly 20 bytes).
* @return The parsed address.
* @throws IllegalArgumentException if the provided string is {@code null}.
* @throws IllegalArgumentException if the string is either not hexadecimal, or not the valid
* representation of a 20 byte address.
*/
public static Address fromHexStringStrict(final String str) {
checkArgument(str != null);
final Bytes value = Bytes.fromHexString(str);
checkArgument(
value.size() == SIZE,
"An account address must be %s bytes long, got %s",
SIZE,
value.size());
return new Address(value);
}
/**
* Precompiled address.
*
* @param value the value
* @return the address
*/
public static Address precompiled(final int value) {
// Allow values up to 0x01FF (511) to encompass layer2 precompile address space
checkArgument(value < 0x01FF, "Precompiled value must be <= 0x01FF");
final byte[] address = new byte[SIZE];
address[SIZE - 2] = (byte) (value >>> 8); // High byte
address[SIZE - 1] = (byte) (value & 0xFF); // Low byte
return new Address(Bytes.wrap(address));
}
/**
* Address of the created contract.
*
* <p>This implement equation (86) in Section 7 of the Yellow Paper (rev. a91c29c).
*
* @param senderAddress the address of the transaction sender.
* @param nonce the nonce of this transaction.
* @return The generated address of the created contract.
*/
public static Address contractAddress(final Address senderAddress, final long nonce) {
return Address.extract(
keccak256(
RLP.encode(
out -> {
out.startList();
out.writeBytes(senderAddress.getBytes());
out.writeLongScalar(nonce);
out.endList();
})));
}
/**
* Returns the hash of the address. Backed by a cache for performance reasons.
*
* @return the hash of the address.
*/
public Hash addressHash() {
try {
return hashCache.get(this);
} catch (ExecutionException e) {
return Hash.hash(getBytes());
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.datatypes;
import java.util.Optional;
/**
* The post-quantum signature algorithms a type-0x50 transaction (AIP-20) may be authorized with.
*
* <p>The identifier, the exact key and signature lengths and the verification gas are the table of
* AIP-20. Only the three FIPS 204 ML-DSA parameter sets are valid in v1; the reserved identifiers
* (SLH-DSA 0x10, Falcon-512 0x20, hybrid flag 0x80) are deliberately absent so that a transaction
* naming them is invalid, not silently accepted.
*/
public enum AerePqAlgorithm {
/** ML-DSA-44, FIPS 204, NIST category 2. */
ML_DSA_44(0x01, 1312, 2420, 55_000L),
/** ML-DSA-65, FIPS 204, NIST category 3, the recommended default. */
ML_DSA_65(0x02, 1952, 3309, 75_000L),
/** ML-DSA-87, FIPS 204, NIST category 5. */
ML_DSA_87(0x03, 2592, 4627, 100_000L);
private final int id;
private final int publicKeyLength;
private final int signatureLength;
private final long verifyGas;
AerePqAlgorithm(
final int id, final int publicKeyLength, final int signatureLength, final long verifyGas) {
this.id = id;
this.publicKeyLength = publicKeyLength;
this.signatureLength = signatureLength;
this.verifyGas = verifyGas;
}
/**
* Looks an algorithm up by its AIP-20 identifier.
*
* @param id the identifier as carried in the transaction
* @return the algorithm, or empty for any identifier this implementation does not accept
*/
public static Optional<AerePqAlgorithm> byId(final int id) {
for (final AerePqAlgorithm a : values()) {
if (a.id == id) {
return Optional.of(a);
}
}
return Optional.empty();
}
/**
* The AIP-20 identifier.
*
* @return the identifier byte value
*/
public int id() {
return id;
}
/**
* The exact public key length in bytes.
*
* @return the length
*/
public int publicKeyLength() {
return publicKeyLength;
}
/**
* The exact signature length in bytes.
*
* @return the length
*/
public int signatureLength() {
return signatureLength;
}
/**
* The gas charged for one signature verification (AIP-20 table, Draft values).
*
* @return the gas
*/
public long verifyGas() {
return verifyGas;
}
}

View File

@ -0,0 +1,110 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.datatypes;
import java.util.Objects;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
/**
* The post-quantum authorization of a type-0x50 transaction (AIP-20): the algorithm, the raw public
* key and the raw signature. No secp256k1 material exists for such a transaction.
*
* @param algorithm the ML-DSA parameter set
* @param publicKey the raw public key, exactly {@code algorithm.publicKeyLength()} bytes
* @param signature the raw signature, exactly {@code algorithm.signatureLength()} bytes
*/
public record AerePqAuthorization(AerePqAlgorithm algorithm, Bytes publicKey, Bytes signature) {
/** The EIP-2718 type byte of the transaction this authorization belongs to. */
public static final byte TYPE_BYTE = (byte) 0x50;
/**
* Validates the record's invariants at construction.
*
* @param algorithm the algorithm
* @param publicKey the public key
* @param signature the signature
*/
public AerePqAuthorization(
final AerePqAlgorithm algorithm, final Bytes publicKey, final Bytes signature) {
this.algorithm = Objects.requireNonNull(algorithm, "algorithm");
this.publicKey = Objects.requireNonNull(publicKey, "publicKey");
this.signature = Objects.requireNonNull(signature, "signature");
}
/**
* Whether the key and signature have exactly the lengths of the algorithm's table. A wrong
* length is an invalid transaction, never a truncated or padded verification.
*
* @return true when both lengths match
*/
public boolean lengthsValid() {
return publicKey.size() == algorithm.publicKeyLength()
&& signature.size() == algorithm.signatureLength();
}
/**
* The bytes charged as authorization data (AIP-20 gas rule).
*
* @return key length plus signature length
*/
public int authorizationByteCount() {
return publicKey.size() + signature.size();
}
/**
* The sender address of AIP-20: {@code keccak256(0x50 || alg_id || public_key)[12:32]}.
*
* @return the derived sender
*/
public Address senderAddress() {
return deriveSender(algorithm, publicKey);
}
/**
* Derives the AIP-20 sender address for a public key under an algorithm.
*
* @param algorithm the algorithm
* @param publicKey the raw public key
* @return the address
*/
public static Address deriveSender(final AerePqAlgorithm algorithm, final Bytes publicKey) {
final Bytes preimage =
Bytes.concatenate(Bytes.of(TYPE_BYTE), Bytes.of((byte) algorithm.id()), publicKey);
return Address.extract(Bytes32.wrap(Hash.hash(preimage).getBytes()));
}
/**
* A copy that shares no byte array with the original (for detached transaction copies).
*
* @return the copy
*/
public AerePqAuthorization detachedCopy() {
return new AerePqAuthorization(algorithm, publicKey.copy(), signature.copy());
}
@Override
public String toString() {
return "AerePqAuthorization{"
+ algorithm
+ ", pk="
+ publicKey.size()
+ "B, sig="
+ signature.size()
+ "B}";
}
}

View File

@ -0,0 +1,300 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.datatypes;
import java.math.BigInteger;
import java.util.List;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
/**
* A transaction is a single cryptographically-signed instruction constructed by an actor externally
* to the scope of Ethereum. While it is assumed that the ultimate external actor will be human in
* nature, software tools will be used in its construction and dissemination.
*
* <p>There are two types of transactions: those which result in message calls and those which
* result in the creation of new accounts with associated code (known informally as ‘contract
* creation’). Message call transactions will have an address present in the {@link #getTo} method
* whereas contract creation transactions will not.
*/
public interface Transaction {
/**
* The Keccak 256-bit hash of this transaction.
*
* @return The Keccak 256-bit hash of this transaction.
*/
Hash getHash();
/**
* A scalar value equal to the number of transactions sent by the sender.
*
* @return the number of transactions sent by the sender.
*/
long getNonce();
/**
* A scalar value equal to the number of Wei to be paid per unit of gas for all computation costs
* incurred as a result of the execution of this transaction.
*
* @return the quantity of Wei per gas unit paid.
*/
Optional<? extends Quantity> getGasPrice();
/**
* A scalar value equal to the number of Wei to be paid on top of base fee, as specified in
* EIP-1559.
*
* @return the quantity of Wei for max fee per gas
*/
default Optional<? extends Quantity> getMaxPriorityFeePerGas() {
return Optional.empty();
}
/**
* A scalar value equal to the number of Wei to be paid in total, as specified in EIP-1559.
*
* @return the quantity of Wei for fee cap.
*/
default Optional<? extends Quantity> getMaxFeePerGas() {
return Optional.empty();
}
/**
* A scalar value equal to the max number of Wei to be paid for blob gas, as specified in
* EIP-4844.
*
* @return the quantity of Wei for fee per blob gas.
*/
default Optional<? extends Quantity> getMaxFeePerBlobGas() {
return Optional.empty();
}
/**
* A scalar value equal to the maximum amount of gas that should be used in executing this
* transaction. This is paid up-front, before any computation is done and may not be increased
* later.
*
* @return the maximum amount of gas that should be used in executing this * transaction.
*/
long getGasLimit();
/**
* The 160-bit address of the message call’s recipient. For a contract creation transaction this
* address will not be present.
*
* @return address of the recipient
*/
Optional<? extends Address> getTo();
/**
* A scalar value equal to the number of Wei to be transferred to the message call’s recipient or,
* in the case of contract creation, as an endowment to the newly created account.
*
* @return value equal to the number of Wei to be transferred
*/
Quantity getValue();
/**
* Value corresponding to the 'yParity' component of non-legacy signatures.
*
* @return the 'yParity' component of the signature
*/
BigInteger getYParity();
/**
* Value corresponding to the 'v' component of legacy signatures, which encodes chainId and
* yParity.
*
* @return the 'V' component of the signature
*/
BigInteger getV();
/**
* Value corresponding to the 'R' component of the signature of the transaction.
*
* @return the 'R' component of the signature
*/
BigInteger getR();
/**
* Value corresponding to the 'S' component of the signature of the transaction.
*
* @return the 'S' component of the signature
*/
BigInteger getS();
/**
* The 160-bit address of the account sending the transaction, extracted from the v, r, s
* parameters.
*
* @return The address of the account that sent this transaction.
*/
Address getSender();
/**
* The chainId, computed from the 'V' portion of the signature. Used for replay protection. If
* replay protection is not enabled, this value will not be present.
*
* @return The chainId for transaction.
*/
Optional<BigInteger> getChainId();
/**
* An unlimited size byte array specifying the EVM-code for the account // initialisation
* procedure.
*
* <p>Only present if this is a contract creation transaction, which is only true if {@link
* #getTo} is empty.
*
* @return if present, the contract init code.
*/
Optional<Bytes> getInit();
/**
* An unlimited size byte array specifying the input data of the message call.
*
* <p>Only present if this is a message call transaction, which is only true if {@link #getTo} is
* present.
*
* @return if present, the message call data
*/
Optional<Bytes> getData();
/**
* The data payload of this transaction.
*
* <p>If this transaction is a message-call to an account (the {@link #getTo} field is present),
* this same value will be exposed by {@link #getData}. If instead this is a contract-creation
* transaction (the {@link #getTo} field is absent), the payload is also exposed by {@link
* #getInit}.
*
* @return the transaction payload
*/
Bytes getPayload();
/**
* The number of zero bytes in the payload.
*
* @return the number of zero bytes in the payload
*/
long getPayloadZeroBytes();
/**
* Returns the type of the transaction.
*
* @return the type of the transaction
*/
TransactionType getType();
/**
* Return the versioned hashes for this transaction.
*
* @return optional list of versioned hashes
*/
Optional<List<VersionedHash>> getVersionedHashes();
/**
* Return the blobs with commitments for this transaction.
*
* @return optional blobs with commitments
*/
Optional<? extends BlobsWithCommitments> getBlobsWithCommitments();
/**
* Return the address of the contract, if the transaction creates one
*
* @return address of new contract or empty otherwise
*/
Optional<Address> contractAddress();
/**
* Return the access list in case of EIP-2930 transaction
*
* @return optional access list
*/
Optional<List<AccessListEntry>> getAccessList();
/**
* Returns the transaction with the proper encoding
*
* @return the encoded transaction as Bytes
*/
Bytes encoded();
/**
* Returns the preimage of the transaction with the proper encoding
*
* @return the encoded preimage of the transaction as Bytes
*/
Bytes encodedPreimage();
/**
* Returns the size in bytes of the encoded transaction. This is the size of the transaction when
* it is announced to other peers. The difference between this and {@link
* #getSizeForBlockInclusion()} is that transactions that are included in a block cannot contain
* blobs, whereas transactions that are announced and sent to other peers have to contain blobs.
*
* @return the size in bytes of the encoded transaction.
*/
int getSizeForAnnouncement();
/**
* Returns the size in bytes of the encoded transaction for block inclusion. This is the size of
* the transaction when it is included in a block. The difference between this and {@link
* #getSizeForAnnouncement()} is that transactions that are included in a block cannot contain
* blobs, whereas transactions that are announced and sent to other peers have to contain blobs.
*
* @return the size in bytes of the encoded transaction for block inclusion.
*/
int getSizeForBlockInclusion();
/**
* Returns whether the transaction is a contract creation
*
* @return {@code true} if this is a contract-creation transaction; otherwise {@code false}
*/
boolean isContractCreation();
/**
* Returns the code delegations if this transaction is a 7702 transaction.
*
* @return the code delegations
*/
Optional<List<CodeDelegation>> getCodeDelegationList();
/**
* Returns the size of the code delegation list.
*
* @return the size of the code delegation list
*/
int codeDelegationListSize();
/**
* The AIP-20 post-quantum authorization, present only for type 0x50 transactions.
*
* @return the authorization, or empty for every other type
*/
default Optional<AerePqAuthorization> getPqAuthorization() {
return Optional.empty();
}
}

View File

@ -0,0 +1,209 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.datatypes;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
/** The enum Transaction type. */
public enum TransactionType {
/** The Frontier. */
FRONTIER(0xf8, 0x00),
/** Access list transaction type. */
ACCESS_LIST(0x01),
/** Eip1559 transaction type. */
EIP1559(0x02),
/** Blob transaction type. */
BLOB(0x03),
/** Eip7702 transaction type. */
DELEGATE_CODE(0x04),
/** AIP-20: Aere post-quantum authorized transaction (ML-DSA), no secp256k1 signature. */
AERE_PQ(0x50);
private static final Set<TransactionType> ACCESS_LIST_SUPPORTED_TRANSACTION_TYPES =
EnumSet.of(ACCESS_LIST, EIP1559, BLOB, DELEGATE_CODE, AERE_PQ);
private static final Set<TransactionType> LEGACY_FEE_MARKET_TRANSACTION_TYPES =
EnumSet.of(FRONTIER, ACCESS_LIST);
// The theoretical boundaries of the first byte of the RLP of a Frontier transaction.
// In practice Frontier transactions almost always start with 0xf8 or 0xf9.
private static final byte MIN_LEGACY_TX_OPAQUE_BYTE = (byte) 0xc0;
private static final byte MAX_LEGACY_TX_OPAQUE_BYTE = (byte) 0xfe;
// This array helps to translate the first opaque byte of an RLP encoded transaction
// to its type.
// Note that Frontier type occupies more slots, since there are different first byte values,
// that represent such type.
// Holes in the array represents invalid first byte values, for which there is not (yet)
// a defined transaction type
private static final TransactionType[] transactionTypeByOpaqueByte =
new TransactionType[Byte.toUnsignedInt(MAX_LEGACY_TX_OPAQUE_BYTE) + 1];
private static final TransactionType[] transactionTypeByEthSerializedType =
// sized by the byte space, not by the enum: AERE_PQ is 0x50 (AIP-20), far past values().length
new TransactionType[256];
static {
EnumSet.allOf(TransactionType.class).stream()
.forEach(
tt -> {
tt.requireChainId = tt != FRONTIER;
tt.supportAccessList = ACCESS_LIST_SUPPORTED_TRANSACTION_TYPES.contains(tt);
tt.supportBaseFeeMarket = !LEGACY_FEE_MARKET_TRANSACTION_TYPES.contains(tt);
tt.supportBlob = tt == BLOB;
tt.supportDelegatedCode = tt == DELEGATE_CODE;
tt.supportPqAuthorization = tt == AERE_PQ;
if (tt == FRONTIER) {
for (int i = Byte.toUnsignedInt(MIN_LEGACY_TX_OPAQUE_BYTE);
i < Byte.toUnsignedInt(MAX_LEGACY_TX_OPAQUE_BYTE);
i++) {
transactionTypeByOpaqueByte[i] = FRONTIER;
}
} else {
transactionTypeByOpaqueByte[tt.getSerializedType()] = tt;
}
transactionTypeByEthSerializedType[tt.getEthSerializedType()] = tt;
});
}
private final byte typeValue;
private final byte serializedType;
boolean requireChainId;
boolean supportAccessList;
boolean supportBaseFeeMarket;
boolean supportBlob;
boolean supportDelegatedCode;
boolean supportPqAuthorization;
TransactionType(final int typeValue, final int serializedType) {
this.typeValue = (byte) typeValue;
this.serializedType = (byte) serializedType;
}
TransactionType(final int typeValue) {
this(typeValue, typeValue);
}
/**
* Gets serialized type.
*
* @return the serialized type
*/
public byte getSerializedType() {
return typeValue;
}
/**
* Gets serialized type for returning in an eth transaction result, factoring in the special case
* for FRONTIER transactions which have enum type 0xf8 but are represented as 0x00 in transaction
* results.
*
* @return the serialized type
*/
public byte getEthSerializedType() {
return serializedType;
}
/**
* Convert from opaque byte to type value.
*
* @param opaqueByte the opaque byte from serialized bytes
* @return the transaction type
*/
public static Optional<TransactionType> fromOpaque(final byte opaqueByte) {
try {
return Optional.ofNullable(transactionTypeByOpaqueByte[Byte.toUnsignedInt(opaqueByte)]);
} catch (final ArrayIndexOutOfBoundsException e) {
return Optional.empty();
}
}
/**
* Convert from ETH serialized byte to type value.
*
* @param ethSerializedType the opaque byte from serialized bytes
* @return the transaction type
*/
public static Optional<TransactionType> fromEthSerializedType(final byte ethSerializedType) {
try {
return Optional.ofNullable(transactionTypeByEthSerializedType[ethSerializedType]);
} catch (final ArrayIndexOutOfBoundsException e) {
return Optional.empty();
}
}
/**
* Does transaction type support access list.
*
* @return the boolean
*/
public boolean supportsAccessList() {
return supportAccessList;
}
/**
* Does transaction type support EIP-1559 fee market.
*
* @return the boolean
*/
public boolean supports1559FeeMarket() {
return supportBaseFeeMarket;
}
/**
* Does transaction type require chain id.
*
* @return the boolean
*/
public boolean requiresChainId() {
return requireChainId;
}
/**
* Does transaction type support data blobs.
*
* @return the boolean
*/
public boolean supportsBlob() {
return supportBlob;
}
/**
* Does transaction type support delegate code.
*
* @return the boolean
*/
public boolean supportsDelegateCode() {
return supportDelegatedCode;
}
/**
* Does transaction type carry an AIP-20 post-quantum authorization instead of a secp256k1
* signature.
*
* @return the boolean
*/
public boolean supportsPqAuthorization() {
return supportPqAuthorization;
}
}

View File

@ -0,0 +1,210 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0007-aere-pq-finality.patch.
*/
package org.hyperledger.besu.ethereum.api.jsonrpc;
import java.util.Collection;
import java.util.HashSet;
public enum RpcMethod {
ADMIN_ADD_PEER("admin_addPeer"),
ADMIN_NODE_INFO("admin_nodeInfo"),
ADMIN_PEERS("admin_peers"),
ADMIN_REMOVE_PEER("admin_removePeer"),
ADMIN_CHANGE_LOG_LEVEL("admin_changeLogLevel"),
ADMIN_GENERATE_LOG_BLOOM_CACHE("admin_generateLogBloomCache"),
ADMIN_LOGS_REPAIR_CACHE("admin_logsRepairCache"),
ADMIN_LOGS_REMOVE_CACHE("admin_logsRemoveCache"),
DEBUG_ACCOUNT_AT("debug_accountAt"),
DEBUG_ACCOUNT_RANGE("debug_accountRange"),
DEBUG_METRICS("debug_metrics"),
DEBUG_RESYNC_WORLDSTATE("debug_resyncWorldState"),
DEBUG_SET_HEAD("debug_setHead"),
DEBUG_REPLAY_BLOCK("debug_replayBlock"),
DEBUG_STORAGE_RANGE_AT("debug_storageRangeAt"),
DEBUG_TRACE_BLOCK("debug_traceBlock"),
DEBUG_TRACE_BLOCK_BY_HASH("debug_traceBlockByHash"),
DEBUG_TRACE_BLOCK_BY_NUMBER("debug_traceBlockByNumber"),
DEBUG_STANDARD_TRACE_BLOCK_TO_FILE("debug_standardTraceBlockToFile"),
DEBUG_STANDARD_TRACE_BAD_BLOCK_TO_FILE("debug_standardTraceBadBlockToFile"),
DEBUG_TRACE_TRANSACTION("debug_traceTransaction"),
DEBUG_TRACE_CALL("debug_traceCall"),
DEBUG_BATCH_RAW_TRANSACTION("debug_batchSendRawTransaction"),
DEBUG_GET_BAD_BLOCKS("debug_getBadBlocks"),
DEBUG_GET_RAW_HEADER("debug_getRawHeader"),
DEBUG_GET_RAW_BLOCK("debug_getRawBlock"),
DEBUG_GET_RAW_RECEIPTS("debug_getRawReceipts"),
DEBUG_GET_RAW_TRANSACTION("debug_getRawTransaction"),
ENGINE_GET_BLOBS_V1("engine_getBlobsV1"),
ENGINE_GET_BLOBS_V2("engine_getBlobsV2"),
ENGINE_GET_BLOBS_V3("engine_getBlobsV3"),
ENGINE_GET_PAYLOAD_V1("engine_getPayloadV1"),
ENGINE_GET_PAYLOAD_V2("engine_getPayloadV2"),
ENGINE_GET_PAYLOAD_V3("engine_getPayloadV3"),
ENGINE_GET_PAYLOAD_V4("engine_getPayloadV4"),
ENGINE_GET_PAYLOAD_V5("engine_getPayloadV5"),
ENGINE_GET_PAYLOAD_V6("engine_getPayloadV6"),
ENGINE_NEW_PAYLOAD_V1("engine_newPayloadV1"),
ENGINE_NEW_PAYLOAD_V2("engine_newPayloadV2"),
ENGINE_NEW_PAYLOAD_V3("engine_newPayloadV3"),
ENGINE_NEW_PAYLOAD_V4("engine_newPayloadV4"),
ENGINE_NEW_PAYLOAD_V5("engine_newPayloadV5"),
ENGINE_FORKCHOICE_UPDATED_V1("engine_forkchoiceUpdatedV1"),
ENGINE_FORKCHOICE_UPDATED_V2("engine_forkchoiceUpdatedV2"),
ENGINE_FORKCHOICE_UPDATED_V3("engine_forkchoiceUpdatedV3"),
ENGINE_FORKCHOICE_UPDATED_V4("engine_forkchoiceUpdatedV4"),
ENGINE_EXCHANGE_TRANSITION_CONFIGURATION("engine_exchangeTransitionConfigurationV1"),
ENGINE_GET_CLIENT_VERSION_V1("engine_getClientVersionV1"),
ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1("engine_getPayloadBodiesByHashV1"),
ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V2("engine_getPayloadBodiesByHashV2"),
ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1("engine_getPayloadBodiesByRangeV1"),
ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V2("engine_getPayloadBodiesByRangeV2"),
ENGINE_EXCHANGE_CAPABILITIES("engine_exchangeCapabilities"),
ENGINE_PREPARE_PAYLOAD_DEBUG("engine_preparePayload_debug"),
ETH_ACCOUNTS("eth_accounts"),
ETH_BLOCK_NUMBER("eth_blockNumber"),
ETH_CALL("eth_call"),
ETH_CHAIN_ID("eth_chainId"),
ETH_COINBASE("eth_coinbase"),
ETH_CONFIG("eth_config"),
ETH_ESTIMATE_GAS("eth_estimateGas"),
ETH_CREATE_ACCESS_LIST("eth_createAccessList"),
ETH_FEE_HISTORY("eth_feeHistory"),
ETH_GAS_PRICE("eth_gasPrice"),
ETH_BLOB_BASE_FEE("eth_blobBaseFee"),
ETH_GET_BALANCE("eth_getBalance"),
ETH_GET_BLOCK_ACCESS_LIST("eth_getBlockAccessList"),
ETH_GET_BLOCK_BY_HASH("eth_getBlockByHash"),
ETH_GET_BLOCK_BY_NUMBER("eth_getBlockByNumber"),
ETH_GET_BLOCK_RECEIPTS("eth_getBlockReceipts"),
ETH_GET_BLOCK_TRANSACTION_COUNT_BY_HASH("eth_getBlockTransactionCountByHash"),
ETH_GET_BLOCK_TRANSACTION_COUNT_BY_NUMBER("eth_getBlockTransactionCountByNumber"),
ETH_GET_CODE("eth_getCode"),
ETH_GET_FILTER_CHANGES("eth_getFilterChanges"),
ETH_GET_FILTER_LOGS("eth_getFilterLogs"),
ETH_GET_LOGS("eth_getLogs"),
ETH_GET_MAX_PRIORITY_FEE_PER_GAS("eth_maxPriorityFeePerGas"),
ETH_GET_PROOF("eth_getProof"),
ETH_GET_STORAGE_AT("eth_getStorageAt"),
ETH_GET_TRANSACTION_BY_BLOCK_HASH_AND_INDEX("eth_getTransactionByBlockHashAndIndex"),
ETH_GET_TRANSACTION_BY_BLOCK_NUMBER_AND_INDEX("eth_getTransactionByBlockNumberAndIndex"),
ETH_GET_TRANSACTION_BY_HASH("eth_getTransactionByHash"),
ETH_GET_TRANSACTION_COUNT("eth_getTransactionCount"),
ETH_GET_TRANSACTION_RECEIPT("eth_getTransactionReceipt"),
ETH_GET_UNCLE_BY_BLOCK_HASH_AND_INDEX("eth_getUncleByBlockHashAndIndex"),
ETH_GET_UNCLE_BY_BLOCK_NUMBER_AND_INDEX("eth_getUncleByBlockNumberAndIndex"),
ETH_GET_UNCLE_COUNT_BY_BLOCK_HASH("eth_getUncleCountByBlockHash"),
ETH_GET_UNCLE_COUNT_BY_BLOCK_NUMBER("eth_getUncleCountByBlockNumber"),
ETH_MINING("eth_mining"),
ETH_NEW_BLOCK_FILTER("eth_newBlockFilter"),
ETH_NEW_FILTER("eth_newFilter"),
ETH_NEW_PENDING_TRANSACTION_FILTER("eth_newPendingTransactionFilter"),
ETH_PROTOCOL_VERSION("eth_protocolVersion"),
ETH_SEND_RAW_TRANSACTION("eth_sendRawTransaction"),
ETH_SEND_TRANSACTION("eth_sendTransaction"),
ETH_SIMULATE_V1("eth_simulateV1"),
ETH_SUBSCRIBE("eth_subscribe"),
ETH_SYNCING("eth_syncing"),
ETH_UNINSTALL_FILTER("eth_uninstallFilter"),
ETH_UNSUBSCRIBE("eth_unsubscribe"),
IBFT_DISCARD_VALIDATOR_VOTE("ibft_discardValidatorVote"),
IBFT_GET_PENDING_VOTES("ibft_getPendingVotes"),
IBFT_GET_VALIDATORS_BY_BLOCK_HASH("ibft_getValidatorsByBlockHash"),
IBFT_GET_VALIDATORS_BY_BLOCK_NUMBER("ibft_getValidatorsByBlockNumber"),
IBFT_PROPOSE_VALIDATOR_VOTE("ibft_proposeValidatorVote"),
IBFT_GET_SIGNER_METRICS("ibft_getSignerMetrics"),
QBFT_DISCARD_VALIDATOR_VOTE("qbft_discardValidatorVote"),
QBFT_GET_PENDING_VOTES("qbft_getPendingVotes"),
QBFT_GET_VALIDATORS_BY_BLOCK_HASH("qbft_getValidatorsByBlockHash"),
QBFT_GET_VALIDATORS_BY_BLOCK_NUMBER("qbft_getValidatorsByBlockNumber"),
QBFT_PROPOSE_VALIDATOR_VOTE("qbft_proposeValidatorVote"),
QBFT_GET_SIGNER_METRICS("qbft_getSignerMetrics"),
QBFT_GET_REQUEST_TIMEOUT_SECONDS("qbft_getRequestTimeoutSeconds"),
// AIP-21 (AERE, 2026-09-18): the per-block post-quantum finality record, read from the seals heard on Commit messages
AERE_GET_PQ_FINALITY("aere_getPqFinality"),
MINER_CHANGE_TARGET_GAS_LIMIT("miner_changeTargetGasLimit"),
MINER_START("miner_start"),
MINER_STOP("miner_stop"),
MINER_GET_MIN_PRIORITY_FEE("miner_getMinPriorityFee"),
MINER_SET_MIN_PRIORITY_FEE("miner_setMinPriorityFee"),
MINER_GET_MIN_GAS_PRICE("miner_getMinGasPrice"),
MINER_SET_MIN_GAS_PRICE("miner_setMinGasPrice"),
MINER_GET_EXTRA_DATA("miner_getExtraData"),
MINER_SET_EXTRA_DATA("miner_setExtraData"),
NET_ENODE("net_enode"),
NET_LISTENING("net_listening"),
NET_PEER_COUNT("net_peerCount"),
NET_SERVICES("net_services"),
NET_VERSION("net_version"),
PERM_ADD_ACCOUNTS_TO_WHITELIST("perm_addAccountsToWhitelist"),
PERM_ADD_ACCOUNTS_TO_ALLOWLIST("perm_addAccountsToAllowlist"),
PERM_ADD_NODES_TO_WHITELIST("perm_addNodesToWhitelist"),
PERM_ADD_NODES_TO_ALLOWLIST("perm_addNodesToAllowlist"),
PERM_GET_ACCOUNTS_WHITELIST("perm_getAccountsWhitelist"),
PERM_GET_ACCOUNTS_ALLOWLIST("perm_getAccountsAllowlist"),
PERM_GET_NODES_WHITELIST("perm_getNodesWhitelist"),
PERM_GET_NODES_ALLOWLIST("perm_getNodesAllowlist"),
PERM_RELOAD_PERMISSIONS_FROM_FILE("perm_reloadPermissionsFromFile"),
PERM_REMOVE_ACCOUNTS_FROM_ALLOWLIST("perm_removeAccountsFromAllowlist"),
PERM_REMOVE_NODES_FROM_ALLOWLIST("perm_removeNodesFromAllowlist"),
RPC_MODULES("rpc_modules"),
TRACE_BLOCK("trace_block"),
TRACE_CALL("trace_call"),
TRACE_CALL_MANY("trace_callMany"),
TRACE_GET("trace_get"),
TRACE_FILTER("trace_filter"),
TRACE_RAW_TRANSACTION("trace_rawTransaction"),
TRACE_REPLAY_BLOCK_TRANSACTIONS("trace_replayBlockTransactions"),
TRACE_TRANSACTION("trace_transaction"),
TX_POOL_BESU_STATISTICS("txpool_besuStatistics"),
TX_POOL_BESU_TRANSACTIONS("txpool_besuTransactions"),
TX_POOL_BESU_PENDING_TRANSACTIONS("txpool_besuPendingTransactions"),
TX_POOL_STATUS("txpool_status"),
TX_POOL_CONTENT_FROM("txpool_contentFrom"),
TX_POOL_CONTENT("txpool_content"),
TX_POOL_INSPECT("txpool_inspect"),
WEB3_CLIENT_VERSION("web3_clientVersion"),
WEB3_SHA3("web3_sha3"),
PLUGINS_RELOAD_CONFIG("plugins_reloadPluginConfig"),
TESTING_BUILD_BLOCK_V1("testing_buildBlockV1");
private final String methodName;
private static final Collection<String> allMethodNames;
public String getMethodName() {
return methodName;
}
static {
allMethodNames = new HashSet<>();
for (RpcMethod m : RpcMethod.values()) {
allMethodNames.add(m.getMethodName());
}
}
RpcMethod(final String methodName) {
this.methodName = methodName;
}
public static boolean rpcMethodExists(final String rpcMethodName) {
return allMethodNames.contains(rpcMethodName);
}
}

View File

@ -11,6 +11,12 @@
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;

View File

@ -11,6 +11,12 @@
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;

View File

@ -11,6 +11,12 @@
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;

View File

@ -11,6 +11,12 @@
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;

View File

@ -0,0 +1,322 @@
/*
* Copyright contributors to Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results;
import org.hyperledger.besu.datatypes.AccessListEntry;
import org.hyperledger.besu.datatypes.BytesHolder;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import java.util.List;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import org.apache.tuweni.bytes.Bytes;
@JsonPropertyOrder({
"accessList",
"authorizationList",
"blockHash",
"blockNumber",
"blockTimestamp",
"chainId",
"from",
"gas",
"gasPrice",
"maxPriorityFeePerGas",
"maxFeePerGas",
"maxFeePerBlobGas",
"hash",
"input",
"nonce",
"to",
"transactionIndex",
"type",
"value",
"yParity",
"v",
"r",
"s",
"algId",
"pqPublicKey",
"pqSignature",
"blobVersionedHashes"
})
public class TransactionBaseResult implements TransactionResult {
@JsonInclude(JsonInclude.Include.NON_NULL)
private final List<AccessListEntry> accessList;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final String chainId;
private final String from;
private final String gas;
private final String gasPrice;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final String maxPriorityFeePerGas;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final String maxFeePerGas;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final String maxFeePerBlobGas;
private final String hash;
private final String input;
private final String nonce;
private final String to;
private final String type;
private final String value;
private final String yParity;
private final String v;
private final String r;
private final String s;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final String algId;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final String pqPublicKey;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final String pqSignature;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final List<String> versionedHashes;
@JsonInclude(JsonInclude.Include.NON_NULL)
private final List<CodeDelegationResult> authorizationList;
public TransactionBaseResult(final Transaction transaction) {
this(transaction, Optional.empty());
}
public TransactionBaseResult(final Transaction transaction, final Optional<Wei> maybeBaseFee) {
final TransactionType transactionType = transaction.getType();
this.accessList =
transaction.getAccessList().orElse(transactionType.supportsAccessList() ? List.of() : null);
this.chainId = transaction.getChainId().map(Quantity::create).orElse(null);
this.from = transaction.getSender().toString();
this.gas = Quantity.create(transaction.getGasLimit());
this.maxPriorityFeePerGas =
transaction.getMaxPriorityFeePerGas().map(Wei::toShortHexString).orElse(null);
this.maxFeePerGas = transaction.getMaxFeePerGas().map(Wei::toShortHexString).orElse(null);
this.maxFeePerBlobGas =
transaction.getMaxFeePerBlobGas().map(Wei::toShortHexString).orElse(null);
this.gasPrice =
Quantity.create(
transaction
.getGasPrice()
.orElseGet(
() ->
maybeBaseFee.isPresent()
? transaction.getEffectiveGasPrice(maybeBaseFee)
: transaction.getMaxFeePerGas().get()));
this.hash = transaction.getHash().toString();
this.input = transaction.getPayload().toString();
this.nonce = Quantity.create(transaction.getNonce());
this.to = transaction.getTo().map(a -> a.getBytes().toHexString()).orElse(null);
if (transactionType == TransactionType.FRONTIER) {
this.type = Quantity.create(0);
this.yParity = null;
this.v = Quantity.create(transaction.getV());
} else {
this.type = Quantity.create(transactionType.getSerializedType());
this.yParity =
transaction.getYParity() == null ? null : Quantity.create(transaction.getYParity());
this.v =
(transactionType == TransactionType.ACCESS_LIST
|| transactionType == TransactionType.EIP1559
|| transactionType == TransactionType.DELEGATE_CODE
|| transactionType == TransactionType.BLOB)
? Quantity.create(transaction.getYParity())
: null;
}
this.value = Quantity.create(transaction.getValue());
this.r = transaction.getR() == null ? null : Quantity.create(transaction.getR());
this.s = transaction.getS() == null ? null : Quantity.create(transaction.getS());
this.algId =
transaction.getPqAuthorization().map(a -> Quantity.create(a.algorithm().id())).orElse(null);
this.pqPublicKey =
transaction.getPqAuthorization().map(a -> a.publicKey().toHexString()).orElse(null);
this.pqSignature =
transaction.getPqAuthorization().map(a -> a.signature().toHexString()).orElse(null);
this.versionedHashes =
transaction
.getVersionedHashes()
.map(
hashes ->
hashes.stream().map(BytesHolder::getBytes).map(Bytes::toHexString).toList())
.orElse(null);
this.authorizationList =
transaction
.getCodeDelegationList()
.map(cds -> cds.stream().map(CodeDelegationResult::new).toList())
.orElse(null);
}
@JsonGetter(value = "accessList")
public List<AccessListEntry> getAccessList() {
return accessList;
}
@JsonGetter(value = "chainId")
public String getChainId() {
return chainId;
}
@JsonGetter(value = "from")
public String getFrom() {
return from;
}
@JsonGetter(value = "gas")
public String getGas() {
return gas;
}
@JsonGetter(value = "gasPrice")
public String getGasPrice() {
return gasPrice;
}
@JsonGetter(value = "maxPriorityFeePerGas")
public String getMaxPriorityFeePerGas() {
return maxPriorityFeePerGas;
}
@JsonGetter(value = "maxFeePerGas")
public String getMaxFeePerGas() {
return maxFeePerGas;
}
@JsonGetter(value = "maxFeePerBlobGas")
public String getMaxFeePerBlobGas() {
return maxFeePerBlobGas;
}
@JsonGetter(value = "hash")
public String getHash() {
return hash;
}
@JsonGetter(value = "input")
public String getInput() {
return input;
}
@JsonGetter(value = "nonce")
public String getNonce() {
return nonce;
}
@JsonGetter(value = "to")
public String getTo() {
return to;
}
@JsonGetter(value = "type")
public String getType() {
return type;
}
@JsonGetter(value = "value")
public String getValue() {
return value;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonGetter(value = "yParity")
public String getYParity() {
return yParity;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonGetter(value = "v")
public String getV() {
return v;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonGetter(value = "r")
public String getR() {
return r;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonGetter(value = "s")
public String getS() {
return s;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonGetter(value = "algId")
public String getAlgId() {
return algId;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonGetter(value = "pqPublicKey")
public String getPqPublicKey() {
return pqPublicKey;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonGetter(value = "pqSignature")
public String getPqSignature() {
return pqSignature;
}
@JsonGetter(value = "blobVersionedHashes")
public List<String> getVersionedHashes() {
return versionedHashes;
}
@JsonGetter(value = "authorizationList")
public List<CodeDelegationResult> getAuthorizationList() {
return authorizationList;
}
@JsonGetter(value = "blockHash")
public String getBlockHash() {
return null;
}
@JsonGetter(value = "blockNumber")
public String getBlockNumber() {
return null;
}
@JsonGetter(value = "blockTimestamp")
public String getBlockTimestamp() {
return null;
}
@JsonGetter(value = "transactionIndex")
public String getTransactionIndex() {
return null;
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.websocket.subscription.pending;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.JsonRpcResult;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity;
import org.hyperledger.besu.ethereum.core.Transaction;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder({
"from",
"gas",
"gasPrice",
"hash",
"input",
"nonce",
"to",
"type",
"value",
"yParity",
"v",
"r",
"s"
})
public class PendingTransactionDetailResult implements JsonRpcResult {
private final String from;
private final String gas;
private final String gasPrice;
private final String hash;
private final String input;
private final String nonce;
private final String to;
private final String type;
private final String value;
private final String yParity;
private final String v;
private final String r;
private final String s;
public PendingTransactionDetailResult(final Transaction tx) {
TransactionType transactionType = tx.getType();
this.from = tx.getSender().toString();
this.gas = Quantity.create(tx.getGasLimit());
this.gasPrice = tx.getGasPrice().map(Quantity::create).orElse(null);
this.hash = tx.getHash().toString();
this.input = tx.getPayload().toString();
this.nonce = Quantity.create(tx.getNonce());
this.to = tx.getTo().map(a -> a.getBytes().toHexString()).orElse(null);
if (transactionType == TransactionType.FRONTIER) {
this.type = Quantity.create(0);
this.yParity = null;
this.v = Quantity.create(tx.getV());
} else {
this.type = Quantity.create(transactionType.getSerializedType());
this.yParity = tx.getYParity() == null ? null : Quantity.create(tx.getYParity());
this.v =
(transactionType == TransactionType.ACCESS_LIST
|| transactionType == TransactionType.EIP1559)
? Quantity.create(tx.getYParity())
: null;
}
this.value = Quantity.create(tx.getValue());
this.r = tx.getR() == null ? null : Quantity.create(tx.getR());
this.s = tx.getS() == null ? null : Quantity.create(tx.getS());
}
@JsonGetter(value = "from")
public String getFrom() {
return from;
}
@JsonGetter(value = "gas")
public String getGas() {
return gas;
}
@JsonGetter(value = "gasPrice")
public String getGasPrice() {
return gasPrice;
}
@JsonGetter(value = "hash")
public String getHash() {
return hash;
}
@JsonGetter(value = "input")
public String getInput() {
return input;
}
@JsonGetter(value = "nonce")
public String getNonce() {
return nonce;
}
@JsonGetter(value = "to")
public String getTo() {
return to;
}
@JsonGetter(value = "type")
public String getType() {
return type;
}
@JsonGetter(value = "value")
public String getValue() {
return value;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonGetter(value = "yParity")
public String getyParity() {
return yParity;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonGetter(value = "v")
public String getV() {
return v;
}
@JsonGetter(value = "r")
public String getR() {
return r;
}
@JsonGetter(value = "s")
public String getS() {
return s;
}
}

View File

@ -11,6 +11,12 @@
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-drop-per-request-console-writes.patch.
*/
package org.hyperledger.besu.ethereum.api.query;
@ -935,7 +941,7 @@ public class BlockchainQueries {
}
}
} catch (final IOException e) {
e.printStackTrace(System.out);
// AERE D-434 (fbfb5f9ecd): removed printStackTrace(System.out) on the cached-log-blooms error path; a cheap malformed log query could otherwise spam the process console, bypassing log4j. LOG.error below records the exception.
LOG.error("Error reading cached log blooms", e);
}
return results;

View File

@ -0,0 +1,111 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.core.encoding;
import org.hyperledger.besu.datatypes.AccessListEntry;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.AerePqAlgorithm;
import org.hyperledger.besu.datatypes.AerePqAuthorization;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPInput;
import java.math.BigInteger;
import org.apache.tuweni.bytes.Bytes;
/**
* Decodes an AIP-20 type-0x50 transaction: {@code 0x50 || rlp([chain_id, nonce,
* max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, alg_id,
* public_key, signature])}.
*
* <p>The decoder is strict on shape: an unknown {@code alg_id} or a key/signature of the wrong
* length is rejected here, before any cryptography runs, with an {@link IllegalArgumentException}
* that the callers of {@link TransactionDecoder} already treat as "malformed transaction".
*/
public class AerePqTransactionDecoder {
private AerePqTransactionDecoder() {
// static
}
/**
* Decodes the opaque bytes of a type-0x50 transaction.
*
* @param input the opaque bytes, type byte included
* @return the transaction
*/
public static Transaction decode(final Bytes input) {
final RLPInput txRlp = RLP.input(input.slice(1)); // skip the transaction type byte
txRlp.enterList();
final BigInteger chainId = txRlp.readBigIntegerScalar();
final Transaction.Builder builder =
Transaction.builder()
.type(TransactionType.AERE_PQ)
.chainId(chainId)
.nonce(txRlp.readLongScalar())
.maxPriorityFeePerGas(Wei.of(txRlp.readUInt256Scalar()))
.maxFeePerGas(Wei.of(txRlp.readUInt256Scalar()))
.gasLimit(txRlp.readLongScalar())
.to(txRlp.readBytes(v -> v.isEmpty() ? null : Address.wrap(v)))
.value(Wei.of(txRlp.readUInt256Scalar()))
.payload(txRlp.readBytes())
.rawRlp(txRlp.raw())
.accessList(
txRlp.readList(
accessListEntryRLPInput -> {
accessListEntryRLPInput.enterList();
final AccessListEntry accessListEntry =
new AccessListEntry(
Address.wrap(accessListEntryRLPInput.readBytes()),
accessListEntryRLPInput.readList(RLPInput::readBytes32));
accessListEntryRLPInput.leaveList();
return accessListEntry;
}))
.sizeForAnnouncement(input.size())
.sizeForBlockInclusion(input.size())
.hash(Hash.hash(input));
final int algId = txRlp.readIntScalar();
final AerePqAlgorithm algorithm =
AerePqAlgorithm.byId(algId)
.orElseThrow(
() ->
new IllegalArgumentException(
"AIP-20: unknown post-quantum algorithm identifier " + algId));
final Bytes publicKey = txRlp.readBytes();
final Bytes signature = txRlp.readBytes();
final AerePqAuthorization authorization =
new AerePqAuthorization(algorithm, publicKey, signature);
if (!authorization.lengthsValid()) {
throw new IllegalArgumentException(
"AIP-20: "
+ algorithm
+ " expects a "
+ algorithm.publicKeyLength()
+ "-byte public key and a "
+ algorithm.signatureLength()
+ "-byte signature, got "
+ publicKey.size()
+ " and "
+ signature.size());
}
final Transaction transaction = builder.pqAuthorization(authorization).build();
txRlp.leaveList();
return transaction;
}
}

View File

@ -0,0 +1,152 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.core.encoding;
import static org.hyperledger.besu.ethereum.core.encoding.AccessListTransactionEncoder.writeAccessList;
import org.hyperledger.besu.datatypes.AccessListEntry;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.AerePqAuthorization;
import org.hyperledger.besu.datatypes.BytesHolder;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPOutput;
import java.math.BigInteger;
import java.util.List;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
/**
* Encodes an AIP-20 type-0x50 transaction and computes its signing pre-image.
*
* <p>Signing pre-image (AIP-20): {@code 0x50 || rlp([chain_id, nonce, max_priority_fee_per_gas,
* max_fee_per_gas, gas_limit, to, value, data, access_list, alg_id, public_key])}; the signing hash
* is its keccak256 and is what ML-DSA signs, in the pure (non pre-hash) interface with an empty
* context string.
*/
public class AerePqTransactionEncoder {
private AerePqTransactionEncoder() {
// static
}
/**
* Writes the payload list of the transaction (everything after the type byte).
*
* @param transaction the transaction, type 0x50
* @param out the RLP output
*/
public static void encode(final Transaction transaction, final RLPOutput out) {
final AerePqAuthorization auth =
transaction
.getPqAuthorization()
.orElseThrow(
() ->
new IllegalStateException(
"type 0x50 transaction without a post-quantum authorization"));
out.startList();
writeUnsignedFields(
transaction.getChainId().orElseThrow(),
transaction.getNonce(),
transaction.getMaxPriorityFeePerGas().orElseThrow(),
transaction.getMaxFeePerGas().orElseThrow(),
transaction.getGasLimit(),
transaction.getTo(),
transaction.getValue(),
transaction.getPayload(),
transaction.getAccessList(),
auth,
out);
out.writeBytes(auth.signature());
out.endList();
}
/**
* The signing pre-image of AIP-20 for the given fields.
*
* @param nonce the nonce
* @param maxPriorityFeePerGas the max priority fee per gas
* @param maxFeePerGas the max fee per gas
* @param gasLimit the gas limit
* @param to the recipient, empty for contract creation
* @param value the value
* @param payload the calldata
* @param chainId the chain id (mandatory for this type)
* @param accessList the access list
* @param authorization the authorization whose algorithm and public key are covered
* @return {@code 0x50 || rlp([...])}
*/
public static Bytes signingPreimage(
final long nonce,
final Wei maxPriorityFeePerGas,
final Wei maxFeePerGas,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final Bytes payload,
final Optional<BigInteger> chainId,
final Optional<List<AccessListEntry>> accessList,
final AerePqAuthorization authorization) {
final Bytes encoded =
RLP.encode(
rlpOutput -> {
rlpOutput.startList();
writeUnsignedFields(
chainId.orElseThrow(
() -> new IllegalStateException("type 0x50 requires a chain id")),
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gasLimit,
to,
value,
payload,
accessList,
authorization,
rlpOutput);
rlpOutput.endList();
});
return Bytes.concatenate(Bytes.of(TransactionType.AERE_PQ.getSerializedType()), encoded);
}
private static void writeUnsignedFields(
final BigInteger chainId,
final long nonce,
final Wei maxPriorityFeePerGas,
final Wei maxFeePerGas,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final Bytes payload,
final Optional<List<AccessListEntry>> accessList,
final AerePqAuthorization authorization,
final RLPOutput out) {
out.writeBigIntegerScalar(chainId);
out.writeLongScalar(nonce);
out.writeUInt256Scalar(maxPriorityFeePerGas);
out.writeUInt256Scalar(maxFeePerGas);
out.writeLongScalar(gasLimit);
out.writeBytes(to.map(BytesHolder::getBytes).map(Bytes::copy).orElse(Bytes.EMPTY));
out.writeUInt256Scalar(value);
out.writeBytes(payload);
writeAccessList(out, accessList);
out.writeIntScalar(authorization.algorithm().id());
out.writeBytes(authorization.publicKey());
}
}

View File

@ -0,0 +1,184 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.ethereum.core.encoding;
import static com.google.common.base.Preconditions.checkNotNull;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPInput;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
public class TransactionDecoder {
@FunctionalInterface
interface Decoder {
Transaction decode(Bytes input);
}
private static final EnumMap<TransactionType, Decoder> TRANSACTION_DECODERS =
new EnumMap<>(
Map.of(
TransactionType.FRONTIER, FrontierTransactionDecoder::decode,
TransactionType.ACCESS_LIST, AccessListTransactionDecoder::decode,
TransactionType.EIP1559, EIP1559TransactionDecoder::decode,
TransactionType.BLOB, BlobTransactionDecoder::decode,
TransactionType.DELEGATE_CODE, CodeDelegationTransactionDecoder::decode,
TransactionType.AERE_PQ, AerePqTransactionDecoder::decode));
private static final EnumMap<TransactionType, Decoder> POOLED_TRANSACTION_DECODERS =
new EnumMap<>(Map.of(TransactionType.BLOB, BlobPooledTransactionDecoder::decode));
/**
* Decodes an RLP input into a transaction. If the input represents a typed transaction, it uses
* the appropriate decoder for that type. Otherwise, it uses the frontier decoder.
*
* @param rlpInput the RLP input
* @return the decoded transaction
*/
public static Transaction decodeRLP(
final RLPInput rlpInput, final EncodingContext encodingContext) {
if (isTypedTransaction(rlpInput)) {
return decodeTransaction(rlpInput, encodingContext);
} else {
return FrontierTransactionDecoder.decode(rlpInput.readAsRlp());
}
}
/**
* Decodes a typed transaction from an RLP input. It first reads the transaction type from the
* input, then uses the appropriate decoder for that type.
*
* @param rlpInput the RLP input
* @return the decoded transaction
*/
private static Transaction decodeTransaction(
final RLPInput rlpInput, final EncodingContext context) {
// Read the typed transaction bytes from the RLP input
final Bytes typedTransactionBytes = rlpInput.readBytes();
// Determine the transaction type from the typed transaction bytes
TransactionType transactionType =
getTransactionType(typedTransactionBytes)
.orElseThrow((() -> new IllegalArgumentException("Unsupported transaction type")));
return decodeTransaction(typedTransactionBytes, transactionType, context);
}
/**
* Decodes a typed transaction. The method first slices the transaction bytes to exclude the
* transaction type, then uses the appropriate decoder for the transaction type to decode the
* remaining bytes.
*
* @param transactionBytes the transaction bytes
* @param transactionType the type of the transaction
* @param context the encoding context
* @return the decoded transaction
*/
private static Transaction decodeTransaction(
final Bytes transactionBytes,
final TransactionType transactionType,
final EncodingContext context) {
// Slice the transaction bytes to exclude the transaction type and prepare for decoding
// Use the appropriate decoder for the transaction type to decode the remaining bytes
return getDecoder(transactionType, context).decode(transactionBytes);
}
/**
* Decodes a transaction from opaque bytes. The method first determines the transaction type from
* the bytes. If the type is present, it delegates the decoding process to the appropriate decoder
* for that type. If the type is not present, it decodes the bytes as an RLP input.
*
* @param opaqueBytes the opaque bytes
* @param context the encoding context
* @return the decoded transaction
*/
public static Transaction decodeOpaqueBytes(
final Bytes opaqueBytes, final EncodingContext context) {
if (opaqueBytes.isEmpty()) {
throw new IllegalArgumentException("Empty opaque bytes");
}
var transactionType = getTransactionType(opaqueBytes);
if (transactionType.isPresent()) {
return decodeTransaction(opaqueBytes, transactionType.get(), context);
} else {
// If the transaction type is not present, decode the opaque bytes as RLP
return decodeRLP(RLP.input(opaqueBytes), context);
}
}
/**
* Retrieves the transaction type from the provided bytes. The method attempts to extract the
* first byte from the input bytes and interpret it as a transaction type. If the byte does not
* correspond to a valid transaction type, the method returns an empty Optional.
*
* @param opaqueBytes the bytes from which to extract the transaction type
* @return an Optional containing the TransactionType if the first byte of the input corresponds
* to a valid transaction type, or an empty Optional if it does not
*/
private static Optional<TransactionType> getTransactionType(final Bytes opaqueBytes) {
byte transactionTypeByte = opaqueBytes.get(0);
return TransactionType.fromOpaque(transactionTypeByte);
}
/**
* Checks if the given RLP input is a typed transaction.
*
* <p>See EIP-2718
*
* <p>If it starts with a value in the range [0, 0x7f] then it is a new transaction type
*
* <p>if it starts with a value in the range [0xc0, 0xfe] then it is a legacy transaction type
*
* @param rlpInput the RLP input
* @return true if the RLP input is a typed transaction, false otherwise
*/
private static boolean isTypedTransaction(final RLPInput rlpInput) {
return !rlpInput.nextIsList();
}
/**
* Gets the decoder for a given transaction type and encoding context. If the context is
* POOLED_TRANSACTION, it uses the network decoder for the type. Otherwise, it uses the typed
* decoder.
*
* @param transactionType the transaction type
* @param encodingContext the encoding context
* @return the decoder
*/
private static Decoder getDecoder(
final TransactionType transactionType, final EncodingContext encodingContext) {
if (encodingContext.equals(EncodingContext.POOLED_TRANSACTION)) {
if (POOLED_TRANSACTION_DECODERS.containsKey(transactionType)) {
return POOLED_TRANSACTION_DECODERS.get(transactionType);
}
}
return checkNotNull(
TRANSACTION_DECODERS.get(transactionType),
"Developer Error. A supported transaction type %s has no associated decoding logic",
transactionType);
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.ethereum.core.encoding;
import static com.google.common.base.Preconditions.checkNotNull;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.rlp.RLPOutput;
import com.google.common.collect.ImmutableMap;
import org.apache.tuweni.bytes.Bytes;
public class TransactionEncoder {
@FunctionalInterface
interface Encoder {
void encode(Transaction transaction, RLPOutput output);
}
private static final ImmutableMap<TransactionType, Encoder> TYPED_TRANSACTION_ENCODERS =
ImmutableMap.of(
TransactionType.ACCESS_LIST,
AccessListTransactionEncoder::encode,
TransactionType.EIP1559,
EIP1559TransactionEncoder::encode,
TransactionType.BLOB,
BlobTransactionEncoder::encode,
TransactionType.DELEGATE_CODE,
CodeDelegationTransactionEncoder::encode,
TransactionType.AERE_PQ,
AerePqTransactionEncoder::encode);
private static final ImmutableMap<TransactionType, Encoder> POOLED_TRANSACTION_ENCODERS =
ImmutableMap.of(TransactionType.BLOB, BlobPooledTransactionEncoder::encode);
/**
* Encodes a transaction into RLP format.
*
* @param transaction the transaction to encode
* @param rlpOutput the RLP output stream
* @param encodingContext the encoding context
*/
public static void encodeRLP(
final Transaction transaction,
final RLPOutput rlpOutput,
final EncodingContext encodingContext) {
final TransactionType transactionType = getTransactionType(transaction);
Bytes opaqueBytes = encodeOpaqueBytes(transaction, encodingContext);
encodeRLP(transactionType, opaqueBytes, rlpOutput);
}
/**
* Encodes a transaction into RLP format.
*
* @param transactionType the type of the transaction
* @param opaqueBytes the bytes of the transaction
* @param rlpOutput the RLP output stream
*/
public static void encodeRLP(
final TransactionType transactionType, final Bytes opaqueBytes, final RLPOutput rlpOutput) {
checkNotNull(transactionType, "Transaction type was not specified.");
if (TransactionType.FRONTIER.equals(transactionType)) {
rlpOutput.writeRaw(opaqueBytes);
} else {
rlpOutput.writeBytes(opaqueBytes);
}
}
/**
* Encodes a transaction into opaque bytes.
*
* @param transaction the transaction to encode
* @param encodingContext the encoding context
* @return the encoded transaction as bytes
*/
public static Bytes encodeOpaqueBytes(
final Transaction transaction, final EncodingContext encodingContext) {
final TransactionType transactionType = getTransactionType(transaction);
if (TransactionType.FRONTIER.equals(transactionType)) {
return RLP.encode(rlpOutput -> FrontierTransactionEncoder.encode(transaction, rlpOutput));
} else {
final Encoder encoder = getEncoder(transactionType, encodingContext);
final BytesValueRLPOutput out = new BytesValueRLPOutput();
transaction
.getRawRlp()
.ifPresentOrElse(
(rawRlp) ->
out.writeRLPBytes(
Bytes.concatenate(Bytes.of(transactionType.getSerializedType()), rawRlp)),
() -> {
out.writeByte(transaction.getType().getSerializedType());
encoder.encode(transaction, out);
});
return out.encoded();
}
}
static void writeSignatureAndV(final Transaction transaction, final RLPOutput out) {
out.writeBigIntegerScalar(transaction.getV());
writeSignature(transaction, out);
}
static void writeSignatureAndRecoveryId(final Transaction transaction, final RLPOutput out) {
out.writeIntScalar(transaction.getSignature().getRecId());
writeSignature(transaction, out);
}
static void writeSignature(final Transaction transaction, final RLPOutput out) {
out.writeBigIntegerScalar(transaction.getSignature().getR());
out.writeBigIntegerScalar(transaction.getSignature().getS());
}
private static TransactionType getTransactionType(final Transaction transaction) {
return checkNotNull(
transaction.getType(), "Transaction type for %s was not specified.", transaction);
}
private static Encoder getEncoder(
final TransactionType transactionType, final EncodingContext encodingContext) {
if (encodingContext.equals(EncodingContext.POOLED_TRANSACTION)) {
if (POOLED_TRANSACTION_ENCODERS.containsKey(transactionType)) {
return POOLED_TRANSACTION_ENCODERS.get(transactionType);
}
}
return checkNotNull(
TYPED_TRANSACTION_ENCODERS.get(transactionType),
"Developer Error. A supported transaction type %s has no associated encoding logic",
transactionType);
}
}

View File

@ -0,0 +1,196 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet;
import org.hyperledger.besu.datatypes.AerePqAlgorithm;
import org.hyperledger.besu.datatypes.AerePqAuthorization;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes32;
import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters;
import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters;
import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* AIP-20 support on the validation path: the activation height and the ML-DSA verification of a
* type-0x50 transaction.
*
* <p>Activation follows every other Aere rule: a per-node system property, {@code
* aere.pqtx.forkBlock}, set uniformly on every node that validates or reads blocks. Absent means
* NEVER: a binary that has this code and no property behaves exactly like one that does not have
* it, which is what makes staging the binary ahead of the height safe.
*
* <p>Verification is FIPS 204 ML-DSA in the pure interface with the empty context string, over the
* 32-byte signing hash of AIP-20. Bouncy Castle's {@link MLDSASigner} implements exactly that
* interface when initialised without a context parameter.
*/
public final class AerePqTransactionSupport {
private static final Logger LOG = LoggerFactory.getLogger(AerePqTransactionSupport.class);
/** The system property that names the activation height; absent means never. */
public static final String FORK_BLOCK_PROPERTY = "aere.pqtx.forkBlock";
private static final long NEVER = Long.MAX_VALUE;
private static final long FORK_BLOCK = readForkBlock();
private AerePqTransactionSupport() {
// static
}
private static long readForkBlock() {
final String raw = System.getProperty(FORK_BLOCK_PROPERTY);
if (raw == null || raw.isBlank()) {
LOG.info("AERE PQ-TX: {} absent, type 0x50 transactions are never accepted", FORK_BLOCK_PROPERTY);
return NEVER;
}
try {
final long value = Long.parseLong(raw.trim());
if (value < 0) {
throw new IllegalArgumentException("negative");
}
LOG.info("AERE PQ-TX: type 0x50 transactions accepted from block {}", value);
return value;
} catch (final IllegalArgumentException e) {
throw new IllegalStateException(
"AERE PQ-TX: "
+ FORK_BLOCK_PROPERTY
+ " must be a non-negative block number, got '"
+ raw
+ "'. Refusing to start with an unreadable consensus parameter.",
e);
}
}
/**
* The configured activation height.
*
* @return the height, or {@link Long#MAX_VALUE} when the type is never active
*/
public static long forkBlock() {
return FORK_BLOCK;
}
/**
* Whether type-0x50 transactions are valid in a block at this height.
*
* @param blockNumber the block number being validated or built
* @return true at or above the activation height
*/
public static boolean isActiveAt(final long blockNumber) {
return blockNumber >= FORK_BLOCK;
}
/**
* The message for a type-0x50 transaction seen below the activation height.
*
* @param blockNumber the block number
* @return a human-readable reason
*/
public static String inactiveMessage(final long blockNumber) {
return FORK_BLOCK == NEVER
? "AIP-20 type 0x50 transactions are not enabled on this node ("
+ FORK_BLOCK_PROPERTY
+ " absent)"
: "AIP-20 type 0x50 transactions are valid from block "
+ FORK_BLOCK
+ ", this is block "
+ blockNumber;
}
/**
* Validates the post-quantum authorization of a type-0x50 transaction: presence, exact lengths,
* chain id presence, and the ML-DSA signature over the AIP-20 signing hash. On success the sender
* is derivable from the public key (no recovery is involved).
*
* @param transaction the transaction, type 0x50
* @return valid, or INVALID_SIGNATURE / INVALID_TRANSACTION_FORMAT with the reason
*/
public static ValidationResult<TransactionInvalidReason> validateAuthorization(
final Transaction transaction) {
final Optional<AerePqAuthorization> maybeAuth = transaction.getPqAuthorization();
if (maybeAuth.isEmpty()) {
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_TRANSACTION_FORMAT,
"type 0x50 transaction carries no post-quantum authorization");
}
final AerePqAuthorization auth = maybeAuth.get();
if (!auth.lengthsValid()) {
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_SIGNATURE,
"post-quantum key or signature length does not match " + auth.algorithm());
}
if (transaction.getChainId().isEmpty()) {
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_TRANSACTION_FORMAT,
"type 0x50 transaction must carry a chain id");
}
final Bytes32 signingHash;
try {
signingHash = Bytes32.wrap(transaction.getPqSigningHash());
} catch (final RuntimeException e) {
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_TRANSACTION_FORMAT,
"type 0x50 signing pre-image could not be computed: " + e.getMessage());
}
final boolean ok;
try {
ok = verify(auth.algorithm(), auth.publicKey().toArrayUnsafe(), signingHash.toArrayUnsafe(), auth.signature().toArrayUnsafe());
} catch (final RuntimeException e) {
// A malformed key that the length check let through: the verifier throws, the tx is invalid.
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_SIGNATURE,
"post-quantum verification failed to run: " + e.getClass().getSimpleName());
}
if (!ok) {
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_SIGNATURE,
"post-quantum signature does not verify over the AIP-20 signing hash");
}
return ValidationResult.valid();
}
/**
* FIPS 204 ML-DSA verification, pure interface, empty context.
*
* @param algorithm the parameter set
* @param publicKey the raw public key
* @param message the message (here the 32-byte signing hash)
* @param signature the raw signature
* @return true when the signature verifies
*/
public static boolean verify(
final AerePqAlgorithm algorithm,
final byte[] publicKey,
final byte[] message,
final byte[] signature) {
final MLDSAParameters parameters =
switch (algorithm) {
case ML_DSA_44 -> MLDSAParameters.ml_dsa_44;
case ML_DSA_65 -> MLDSAParameters.ml_dsa_65;
case ML_DSA_87 -> MLDSAParameters.ml_dsa_87;
};
final MLDSAPublicKeyParameters pub = new MLDSAPublicKeyParameters(parameters, publicKey);
final MLDSASigner verifier = new MLDSASigner();
verifier.init(false, pub);
verifier.update(message, 0, message.length);
return verifier.verifySignature(signature);
}
}

View File

@ -0,0 +1,877 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.ethereum.mainnet;
import static org.hyperledger.besu.evm.internal.Words.clampedAdd;
import static org.hyperledger.besu.evm.worldstate.CodeDelegationHelper.getTarget;
import static org.hyperledger.besu.evm.worldstate.CodeDelegationHelper.hasCodeDelegation;
import org.hyperledger.besu.datatypes.AccessListEntry;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.feemarket.CoinbaseFeePriceCalculator;
import org.hyperledger.besu.ethereum.mainnet.block.access.list.AccessLocationTracker;
import org.hyperledger.besu.ethereum.mainnet.block.access.list.PartialBlockAccessView;
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket;
import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult;
import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason;
import org.hyperledger.besu.ethereum.trie.MerkleTrieException;
import org.hyperledger.besu.evm.Code;
import org.hyperledger.besu.evm.account.Account;
import org.hyperledger.besu.evm.account.MutableAccount;
import org.hyperledger.besu.evm.blockhash.BlockHashLookup;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;
import org.hyperledger.besu.evm.log.TransferLogEmitter;
import org.hyperledger.besu.evm.processor.AbstractMessageProcessor;
import org.hyperledger.besu.evm.processor.ContractCreationProcessor;
import org.hyperledger.besu.evm.processor.MessageCallProcessor;
import org.hyperledger.besu.evm.tracing.OperationTracer;
import org.hyperledger.besu.evm.worldstate.CodeDelegationHelper;
import org.hyperledger.besu.evm.worldstate.WorldUpdater;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MainnetTransactionProcessor {
private static final Logger LOG = LoggerFactory.getLogger(MainnetTransactionProcessor.class);
private static final Set<Address> EMPTY_ADDRESS_SET = Set.of();
protected final GasCalculator gasCalculator;
protected final TransactionValidatorFactory transactionValidatorFactory;
private final ContractCreationProcessor contractCreationProcessor;
private final MessageCallProcessor messageCallProcessor;
private final int maxStackSize;
private final boolean clearEmptyAccounts;
protected final boolean warmCoinbase;
protected final FeeMarket feeMarket;
private final CoinbaseFeePriceCalculator coinbaseFeePriceCalculator;
private final Optional<CodeDelegationProcessor> maybeCodeDelegationProcessor;
private final TransferLogEmitter transferLogEmitter;
private MainnetTransactionProcessor(
final GasCalculator gasCalculator,
final TransactionValidatorFactory transactionValidatorFactory,
final ContractCreationProcessor contractCreationProcessor,
final MessageCallProcessor messageCallProcessor,
final boolean clearEmptyAccounts,
final boolean warmCoinbase,
final int maxStackSize,
final FeeMarket feeMarket,
final CoinbaseFeePriceCalculator coinbaseFeePriceCalculator,
final CodeDelegationProcessor maybeCodeDelegationProcessor,
final TransferLogEmitter transferLogEmitter) {
this.gasCalculator = gasCalculator;
this.transactionValidatorFactory = transactionValidatorFactory;
this.contractCreationProcessor = contractCreationProcessor;
this.messageCallProcessor = messageCallProcessor;
this.clearEmptyAccounts = clearEmptyAccounts;
this.warmCoinbase = warmCoinbase;
this.maxStackSize = maxStackSize;
this.feeMarket = feeMarket;
this.coinbaseFeePriceCalculator = coinbaseFeePriceCalculator;
this.maybeCodeDelegationProcessor = Optional.ofNullable(maybeCodeDelegationProcessor);
this.transferLogEmitter = transferLogEmitter;
}
/**
* Applies a transaction to the current system state.
*
* @param worldState The current world state
* @param blockHeader The current block header
* @param transaction The transaction to process
* @param miningBeneficiary The address which is to receive the transaction fee
* @param blockHashLookup The {@link BlockHashLookup} to use for BLOCKHASH operations
* @param transactionValidationParams Validation parameters that will be used by the {@link
* MainnetTransactionValidator}
* @return the transaction result
* @see MainnetTransactionValidator
* @see TransactionValidationParams
*/
public TransactionProcessingResult processTransaction(
final WorldUpdater worldState,
final ProcessableBlockHeader blockHeader,
final Transaction transaction,
final Address miningBeneficiary,
final BlockHashLookup blockHashLookup,
final TransactionValidationParams transactionValidationParams,
final Wei blobGasPrice) {
return processTransaction(
worldState,
blockHeader,
transaction,
miningBeneficiary,
OperationTracer.NO_TRACING,
blockHashLookup,
transactionValidationParams,
blobGasPrice);
}
/**
* Applies a transaction to the current system state.
*
* @param worldState The current world state
* @param blockHeader The current block header
* @param transaction The transaction to process
* @param miningBeneficiary The address which is to receive the transaction fee
* @param operationTracer The tracer to record results of each EVM operation
* @param blockHashLookup The {@link BlockHashLookup} to use for BLOCKHASH operations
* @return the transaction result
*/
public TransactionProcessingResult processTransaction(
final WorldUpdater worldState,
final ProcessableBlockHeader blockHeader,
final Transaction transaction,
final Address miningBeneficiary,
final OperationTracer operationTracer,
final BlockHashLookup blockHashLookup,
final Wei blobGasPrice) {
return processTransaction(
worldState,
blockHeader,
transaction,
miningBeneficiary,
operationTracer,
blockHashLookup,
ImmutableTransactionValidationParams.builder().build(),
blobGasPrice);
}
public TransactionProcessingResult processTransaction(
final WorldUpdater worldState,
final ProcessableBlockHeader blockHeader,
final Transaction transaction,
final Address miningBeneficiary,
final OperationTracer operationTracer,
final BlockHashLookup blockHashLookup,
final TransactionValidationParams transactionValidationParams,
final Wei blobGasPrice) {
return processTransaction(
worldState,
blockHeader,
transaction,
miningBeneficiary,
operationTracer,
blockHashLookup,
transactionValidationParams,
blobGasPrice,
Optional.empty());
}
public TransactionProcessingResult processTransaction(
final WorldUpdater worldState,
final ProcessableBlockHeader blockHeader,
final Transaction transaction,
final Address miningBeneficiary,
final OperationTracer operationTracer,
final BlockHashLookup blockHashLookup,
final TransactionValidationParams transactionValidationParams,
final Wei blobGasPrice,
final Optional<AccessLocationTracker> accessLocationTracker) {
try {
final var transactionValidator = transactionValidatorFactory.get();
LOG.trace("Starting execution of {}", transaction);
// AIP-20: below the activation height the type is invalid in blocks, on every node alike.
if (transaction.getType().supportsPqAuthorization()
&& !AerePqTransactionSupport.isActiveAt(blockHeader.getNumber())) {
return TransactionProcessingResult.invalid(
ValidationResult.invalid(
TransactionInvalidReason.INVALID_TRANSACTION_FORMAT,
AerePqTransactionSupport.inactiveMessage(blockHeader.getNumber())));
}
ValidationResult<TransactionInvalidReason> validationResult =
transactionValidator.validate(
transaction,
blockHeader.getBaseFee(),
Optional.ofNullable(blobGasPrice),
transactionValidationParams);
// Make sure the transaction is intrinsically valid before trying to
// compare against a sender account (because the transaction may not
// be signed correctly to extract the sender).
if (!validationResult.isValid()) {
LOG.debug("Invalid transaction: {}", validationResult.getErrorMessage());
return TransactionProcessingResult.invalid(validationResult);
}
final Address senderAddress = transaction.getSender();
final MutableAccount sender = worldState.getOrCreateSenderAccount(senderAddress);
accessLocationTracker.ifPresent(t -> t.addTouchedAccount(senderAddress));
validationResult =
transactionValidator.validateForSender(transaction, sender, transactionValidationParams);
if (!validationResult.isValid()) {
LOG.debug("Invalid transaction: {}", validationResult.getErrorMessage());
return TransactionProcessingResult.invalid(validationResult);
}
operationTracer.tracePrepareTransaction(worldState, transaction);
final Set<Address> eip2930WarmAddressList = new HashSet<>(Address.SIZE);
final long previousNonce = sender.incrementNonce();
LOG.trace(
"Incremented sender {} nonce ({} -> {})",
senderAddress,
previousNonce,
sender.getNonce());
final Wei transactionGasPrice =
feeMarket.getTransactionPriceCalculator().price(transaction, blockHeader.getBaseFee());
final long blobGas = gasCalculator.blobGasCost(transaction.getBlobCount());
final Wei upfrontGasCost =
transaction.getUpfrontGasCost(transactionGasPrice, blobGasPrice, blobGas);
try {
final Wei previousBalance = sender.decrementBalance(upfrontGasCost);
LOG.trace(
"Deducted sender {} upfront gas cost {} ({} -> {})",
senderAddress,
upfrontGasCost,
previousBalance,
sender.getBalance());
} catch (final IllegalStateException ise) {
if (transactionValidationParams.allowUnderpriced()) {
LOG.trace("Allowing account balance underflow as requested");
} else {
throw ise;
}
}
long codeDelegationRefund = 0L;
long alreadyExistingDelegators = 0L;
if (transaction.getType().equals(TransactionType.DELEGATE_CODE)) {
if (maybeCodeDelegationProcessor.isEmpty()) {
throw new RuntimeException("Code delegation processor is required for 7702 transactions");
}
final WorldUpdater delegationUpdater = worldState.updater();
final CodeDelegationResult codeDelegationResult =
maybeCodeDelegationProcessor
.get()
.process(delegationUpdater, transaction, accessLocationTracker);
eip2930WarmAddressList.addAll(codeDelegationResult.accessedDelegatorAddresses());
alreadyExistingDelegators = codeDelegationResult.alreadyExistingDelegators();
codeDelegationRefund =
gasCalculator.calculateDelegateCodeGasRefund(alreadyExistingDelegators);
delegationUpdater.commit();
}
final List<AccessListEntry> eip2930AccessListEntries =
transaction.getAccessList().orElse(List.of());
// we need to keep a separate hash set of addresses in case they specify no storage.
// No-storage is a common pattern, especially for Externally Owned Accounts
final Multimap<Address, Bytes32> eip2930StorageList = HashMultimap.create();
int accessListStorageCount = 0;
for (final var entry : eip2930AccessListEntries) {
final Address address = entry.address();
eip2930WarmAddressList.add(address);
final List<Bytes32> storageKeys = entry.storageKeys();
eip2930StorageList.putAll(address, storageKeys);
accessListStorageCount += storageKeys.size();
}
if (warmCoinbase) {
eip2930WarmAddressList.add(miningBeneficiary);
}
final long accessListGas =
gasCalculator.accessListGasCost(eip2930AccessListEntries.size(), accessListStorageCount);
final long codeDelegationGas =
gasCalculator.delegateCodeGasCost(transaction.codeDelegationListSize());
final long intrinsicRegularGas =
gasCalculator.transactionIntrinsicGasCost(
transaction, clampedAdd(accessListGas, codeDelegationGas));
// EIP-8037: Validate that gas limit covers both regular AND state intrinsic gas.
// This must be checked before frame construction to reject the tx at the intrinsic level.
final var stateGasCalc = gasCalculator.stateGasCostCalculator();
final long intrinsicStateGas =
stateGasCalc.transactionIntrinsicStateGas(
blockHeader.getGasLimit(),
transaction.isContractCreation(),
transaction.codeDelegationListSize());
if (transaction.getGasLimit() < intrinsicRegularGas + intrinsicStateGas) {
LOG.trace(
"Insufficient gas for intrinsic cost: gasLimit={}, regularIntrinsic={}, stateIntrinsic={}",
transaction.getGasLimit(),
intrinsicRegularGas,
intrinsicStateGas);
return TransactionProcessingResult.invalid(
ValidationResult.invalid(
TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT,
String.format(
"intrinsic gas cost %d (regular %d + state %d) exceeds gas limit %d",
intrinsicRegularGas + intrinsicStateGas,
intrinsicRegularGas,
intrinsicStateGas,
transaction.getGasLimit())));
}
final long gasAvailable = transaction.getGasLimit() - intrinsicRegularGas;
LOG.trace(
"Gas available for execution {} = {} - {} (limit - intrinsic)",
gasAvailable,
transaction.getGasLimit(),
intrinsicRegularGas);
final WorldUpdater worldUpdater = worldState.updater();
operationTracer.traceStartTransaction(worldUpdater, transaction);
final MessageFrame.Builder commonMessageFrameBuilder =
MessageFrame.builder()
.maxStackSize(maxStackSize)
.worldUpdater(worldUpdater.updater())
.initialGas(gasAvailable)
.originator(senderAddress)
.gasPrice(transactionGasPrice)
.blobGasPrice(blobGasPrice)
.sender(senderAddress)
.value(transaction.getValue())
.apparentValue(transaction.getValue())
.blockValues(blockHeader)
.completer(__ -> {})
.miningBeneficiary(miningBeneficiary)
.blockHashLookup(blockHashLookup)
.eip2930AccessListWarmStorage(eip2930StorageList);
accessLocationTracker.ifPresent(commonMessageFrameBuilder::eip7928AccessList);
if (transaction.getVersionedHashes().isPresent()) {
commonMessageFrameBuilder.versionedHashes(
Optional.of(transaction.getVersionedHashes().get().stream().toList()));
} else {
commonMessageFrameBuilder.versionedHashes(Optional.empty());
}
final MessageFrame initialFrame;
if (transaction.isContractCreation()) {
final Address contractAddress =
Address.contractAddress(senderAddress, sender.getNonce() - 1L);
accessLocationTracker.ifPresent(t -> t.addTouchedAccount(contractAddress));
final Bytes initCodeBytes = transaction.getPayload();
Code code = new Code(initCodeBytes);
initialFrame =
commonMessageFrameBuilder
.type(MessageFrame.Type.CONTRACT_CREATION)
.address(contractAddress)
.contract(contractAddress)
.inputData(initCodeBytes.slice(code.getSize()))
.code(code)
.eip2930AccessListWarmAddresses(eip2930WarmAddressList)
.build();
} else {
@SuppressWarnings("OptionalGetWithoutIsPresent") // isContractCall tests isPresent
final Address to = transaction.getTo().get();
accessLocationTracker.ifPresent(t -> t.addTouchedAccount(to));
final Code code =
processCodeFromAccount(
worldState, eip2930WarmAddressList, worldState.get(to), accessLocationTracker);
initialFrame =
commonMessageFrameBuilder
.type(MessageFrame.Type.MESSAGE_CALL)
.address(to)
.contract(to)
.inputData(transaction.getPayload())
.code(code)
.eip2930AccessListWarmAddresses(eip2930WarmAddressList)
.build();
}
// EIP-8037: Initialize the state gas reservoir for Amsterdam+ forks.
// When multidimensional gas is active, regular gas is capped at TX_MAX_GAS_LIMIT - intrinsic.
// Gas beyond that cap goes into the state gas reservoir.
if (stateGasCalc.isActive()) {
final long regularBudget =
Math.max(0L, stateGasCalc.transactionRegularGasLimit() - intrinsicRegularGas);
final long gasLeft = Math.min(regularBudget, gasAvailable);
final long reservoir = gasAvailable - gasLeft;
initialFrame.setGasRemaining(gasLeft);
initialFrame.setStateGasReservoir(reservoir);
}
// EIP-8037: Charge state gas for intrinsic costs
if (transaction.isContractCreation()) {
stateGasCalc.chargeCreateStateGas(initialFrame);
}
if (transaction.getType().equals(TransactionType.DELEGATE_CODE)) {
stateGasCalc.chargeCodeDelegationStateGas(
initialFrame, transaction.codeDelegationListSize(), alreadyExistingDelegators);
}
// EIP-8037: Advance the undo mark so intrinsic state gas charges (auth delegation,
// contract creation) are not rolled back if the initial frame's execution reverts.
// These are transaction-level costs that persist regardless of execution outcome.
initialFrame.advanceUndoMark();
Deque<MessageFrame> messageFrameStack = initialFrame.getMessageFrameStack();
// EIP-8037: Track spillBurned before the initial frame's final processing step.
// When the initial frame reverts/halts, its state gas spill should count as state gas
// for block accounting. Child frame spills (tracked earlier) should not.
long spillBurnedBeforeInitialFinal = 0;
while (!messageFrameStack.isEmpty()) {
if (messageFrameStack.size() == 1) {
spillBurnedBeforeInitialFinal = initialFrame.getStateGasSpillBurned();
}
process(messageFrameStack.peekFirst(), operationTracer);
}
final long initialFrameStateGasSpill =
initialFrame.getStateGasSpillBurned() - spillBurnedBeforeInitialFinal;
// EIP-8037: Runtime TX_MAX_GAS_LIMIT enforcement on regular gas only.
// With multidimensional gas, tx.gasLimit can exceed TX_MAX_GAS_LIMIT to accommodate
// state gas, but regular gas consumption is still bounded at runtime.
// For pre-Amsterdam forks, transactionRegularGasLimit() returns Long.MAX_VALUE (always
// passes).
// We also need to include leftover reservoir in remaining gas for correct consumption
// calculation
final long totalRemaining =
initialFrame.getRemainingGas() + initialFrame.getStateGasReservoir();
final long totalConsumed = transaction.getGasLimit() - totalRemaining;
final long regularConsumed = totalConsumed - initialFrame.getStateGasUsed();
final boolean regularGasLimitExceeded =
regularConsumed > stateGasCalc.transactionRegularGasLimit();
if (regularGasLimitExceeded) {
LOG.debug(
"Transaction {} regular gas {} exceeds TX_MAX_GAS_LIMIT {}, reverting",
transaction.getHash(),
regularConsumed,
stateGasCalc.transactionRegularGasLimit());
}
final boolean txSucceeded =
initialFrame.getState() == MessageFrame.State.COMPLETED_SUCCESS
&& !regularGasLimitExceeded;
if (txSucceeded) {
worldUpdater.commit();
} else {
if (initialFrame.getExceptionalHaltReason().isPresent()) {
validationResult =
ValidationResult.invalid(
TransactionInvalidReason.EXECUTION_HALTED,
initialFrame.getExceptionalHaltReason().get().getDescription());
}
if (regularGasLimitExceeded) {
validationResult =
ValidationResult.invalid(
TransactionInvalidReason.EXECUTION_HALTED,
"Regular gas consumption exceeds TX_MAX_GAS_LIMIT");
}
}
// TODO SLD are the log correct following EIP-7623?
if (LOG.isTraceEnabled()) {
LOG.trace(
"Gas used by transaction: {}, by message call/contract creation: {}",
transaction.getGasLimit() - initialFrame.getRemainingGas(),
gasAvailable - initialFrame.getRemainingGas());
}
// Refund the sender by what we should and pay the miner fee (note that we're doing them one
// after the other so that if it is the same account somehow, we end up with the right result)
// EIP-8037: No refund when regular gas limit is exceeded (all gas consumed)
final long refundedGas =
regularGasLimitExceeded
? 0L
: gasCalculator.calculateGasRefund(transaction, initialFrame, codeDelegationRefund);
final Wei refundedWei = transactionGasPrice.multiply(refundedGas);
final Wei balancePriorToRefund = sender.getBalance();
sender.incrementBalance(refundedWei);
LOG.atTrace()
.setMessage("refunded sender {} {} wei ({} -> {})")
.addArgument(senderAddress)
.addArgument(refundedWei)
.addArgument(balancePriorToRefund)
.addArgument(sender.getBalance())
.log();
// Calculate gas used: max of execution gas and transaction floor cost (EIP-7623)
// For pre-Prague forks, floor cost is 0, so this returns just execution gas
// For Prague+ forks with EIP-7778, this ensures block gas accounts for data floor
// EIP-8037: Gas accounting with multidimensional gas support
final long floorCost =
gasCalculator.transactionFloorCost(
transaction.getPayload(), transaction.getPayloadZeroBytes());
final TransactionGasAccounting.GasResult gasResult =
TransactionGasAccounting.builder()
.txGasLimit(transaction.getGasLimit())
.remainingGas(initialFrame.getRemainingGas())
.stateGasReservoir(initialFrame.getStateGasReservoir())
.stateGasUsed(initialFrame.getStateGasUsed())
.initialFrameStateGasSpill(initialFrameStateGasSpill)
.stateGasSpillBurned(initialFrame.getStateGasSpillBurned())
.initialFrameRegularHaltBurn(initialFrame.getInitialFrameRegularHaltBurn())
.refundedGas(refundedGas)
.floorCost(floorCost)
.regularGasLimitExceeded(regularGasLimitExceeded)
.build()
.calculate();
final long effectiveStateGas = gasResult.effectiveStateGas();
final long gasUsedByTransaction = gasResult.gasUsedByTransaction();
final long usedGas = gasResult.usedGas();
final CoinbaseFeePriceCalculator coinbaseCalculator;
if (blockHeader.getBaseFee().isPresent()) {
final Wei baseFee = blockHeader.getBaseFee().get();
final boolean gasPriceBelowBaseFee = transactionGasPrice.compareTo(baseFee) < 0;
if (transactionValidationParams.allowUnderpriced()
|| transactionValidationParams.isPreserveCallerGasPricing()) {
coinbaseCalculator =
gasPriceBelowBaseFee ? (a, b, c) -> Wei.ZERO : coinbaseFeePriceCalculator;
} else {
if (gasPriceBelowBaseFee) {
final Optional<PartialBlockAccessView> partialBlockAccessView =
accessLocationTracker.map(
tracker -> tracker.createPartialBlockAccessView(worldState));
return TransactionProcessingResult.failed(
gasUsedByTransaction,
refundedGas,
usedGas,
effectiveStateGas,
ValidationResult.invalid(
TransactionInvalidReason.TRANSACTION_PRICE_TOO_LOW,
"transaction price must be greater than base fee"),
Optional.empty(),
Optional.empty(),
partialBlockAccessView);
}
coinbaseCalculator = coinbaseFeePriceCalculator;
}
} else {
coinbaseCalculator = CoinbaseFeePriceCalculator.frontier();
}
final Wei coinbaseWeiDelta =
coinbaseCalculator.price(usedGas, transactionGasPrice, blockHeader.getBaseFee());
operationTracer.traceBeforeRewardTransaction(worldUpdater, transaction, coinbaseWeiDelta);
// EIP-158 & EIP-7928: coinbase is considered "touched" even when fees are zero.
// Touching ensures an *empty* coinbase can be deleted during state clearing.
final MutableAccount coinbase = worldState.getOrCreate(miningBeneficiary);
accessLocationTracker.ifPresent(t -> t.addTouchedAccount(miningBeneficiary));
if (!coinbaseWeiDelta.isZero()) {
coinbase.incrementBalance(coinbaseWeiDelta);
}
// EIP-7708: Emit closure logs for accounts with remaining balance before deletion
// Noop before Amsterdam
transferLogEmitter.emitClosureLogs(
worldState, initialFrame.getSelfDestructs(), initialFrame::addLog);
operationTracer.traceEndTransaction(
worldState.updater(),
transaction,
txSucceeded,
initialFrame.getOutputData(),
initialFrame.getLogs(),
gasUsedByTransaction,
initialFrame.getSelfDestructs(),
0L);
initialFrame.getSelfDestructs().forEach(worldState::deleteAccount);
if (clearEmptyAccounts) {
worldState.clearAccountsThatAreEmpty();
}
final Optional<PartialBlockAccessView> partialBlockAccessView =
accessLocationTracker.map(tracker -> tracker.createPartialBlockAccessView(worldState));
if (txSucceeded) {
return TransactionProcessingResult.successful(
initialFrame.getLogs(),
gasUsedByTransaction,
refundedGas,
usedGas,
effectiveStateGas,
initialFrame.getOutputData(),
partialBlockAccessView,
validationResult);
} else {
if (initialFrame.getExceptionalHaltReason().isPresent()) {
LOG.debug(
"Transaction {} processing halted: {}",
transaction.getHash(),
initialFrame.getExceptionalHaltReason().get());
}
if (initialFrame.getRevertReason().isPresent()) {
LOG.debug(
"Transaction {} reverted: {}",
transaction.getHash(),
initialFrame.getRevertReason().get());
}
return TransactionProcessingResult.failed(
gasUsedByTransaction,
refundedGas,
usedGas,
effectiveStateGas,
validationResult,
initialFrame.getRevertReason(),
initialFrame.getExceptionalHaltReason(),
partialBlockAccessView);
}
} catch (final MerkleTrieException re) {
operationTracer.traceEndTransaction(
worldState.updater(),
transaction,
false,
Bytes.EMPTY,
List.of(),
0,
EMPTY_ADDRESS_SET,
0L);
// need to throw to trigger the heal
throw re;
} catch (final RuntimeException re) {
final var cause = re.getCause();
// in case of an interruption then just return without calling any other tracing method
if (cause != null && cause instanceof InterruptedException) {
LOG.atDebug()
.setMessage("Interrupted while processing the transaction with hash {}")
.addArgument(transaction::getHash)
.log();
return TransactionProcessingResult.invalid(
ValidationResult.invalid(TransactionInvalidReason.EXECUTION_INTERRUPTED));
}
operationTracer.traceEndTransaction(
worldState.updater(),
transaction,
false,
Bytes.EMPTY,
List.of(),
0,
EMPTY_ADDRESS_SET,
0L);
LOG.error("Critical Exception Processing Transaction", re);
return TransactionProcessingResult.invalid(
ValidationResult.invalid(
TransactionInvalidReason.INTERNAL_ERROR,
"Internal Error in Besu - " + re + "\n" + printableStackTraceFromThrowable(re)));
}
}
public void process(final MessageFrame frame, final OperationTracer operationTracer) {
final AbstractMessageProcessor executor = getMessageProcessor(frame.getType());
executor.process(frame, operationTracer);
}
public AbstractMessageProcessor getMessageProcessor(final MessageFrame.Type type) {
return switch (type) {
case MESSAGE_CALL -> messageCallProcessor;
case CONTRACT_CREATION -> contractCreationProcessor;
};
}
public MessageCallProcessor getMessageCallProcessor() {
return messageCallProcessor;
}
public boolean getClearEmptyAccounts() {
return clearEmptyAccounts;
}
private String printableStackTraceFromThrowable(final RuntimeException re) {
final StringBuilder builder = new StringBuilder();
for (final StackTraceElement stackTraceElement : re.getStackTrace()) {
builder.append("\tat ").append(stackTraceElement.toString()).append("\n");
}
return builder.toString();
}
private Code processCodeFromAccount(
final WorldUpdater worldUpdater,
final Set<Address> warmAddressList,
final Account contract,
final Optional<AccessLocationTracker> accessLocationTracker) {
if (contract == null) {
return Code.EMPTY_CODE;
}
final Hash codeHash = contract.getCodeHash();
if (codeHash == null || codeHash.equals(Hash.EMPTY)) {
return Code.EMPTY_CODE;
}
if (hasCodeDelegation(contract.getCode())) {
return delegationTargetCode(worldUpdater, warmAddressList, contract, accessLocationTracker);
}
// Bonsai accounts may have a fully cached code, so we use that one
if (contract.getCodeCache() != null) {
return contract.getOrCreateCachedCode();
}
// Any other account can only use the cached jump dest analysis if available
return messageCallProcessor.getOrCreateCachedJumpDest(
contract.getCodeHash(), contract.getCode());
}
private Code delegationTargetCode(
final WorldUpdater worldUpdater,
final Set<Address> warmAddressList,
final Account contract,
final Optional<AccessLocationTracker> accessLocationTracker) {
// we need to look up the target account and its code, but do NOT charge gas for it
final CodeDelegationHelper.Target target =
getTarget(worldUpdater, gasCalculator::isPrecompile, contract, accessLocationTracker);
warmAddressList.add(target.address());
return target.code();
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private GasCalculator gasCalculator;
private TransactionValidatorFactory transactionValidatorFactory;
private ContractCreationProcessor contractCreationProcessor;
private MessageCallProcessor messageCallProcessor;
private boolean clearEmptyAccounts;
private boolean warmCoinbase;
private int maxStackSize;
private FeeMarket feeMarket;
private CoinbaseFeePriceCalculator coinbaseFeePriceCalculator;
private CodeDelegationProcessor codeDelegationProcessor;
private TransferLogEmitter transferLogEmitter = TransferLogEmitter.NOOP;
public Builder gasCalculator(final GasCalculator gasCalculator) {
this.gasCalculator = gasCalculator;
return this;
}
public Builder transactionValidatorFactory(
final TransactionValidatorFactory transactionValidatorFactory) {
this.transactionValidatorFactory = transactionValidatorFactory;
return this;
}
public Builder contractCreationProcessor(
final ContractCreationProcessor contractCreationProcessor) {
this.contractCreationProcessor = contractCreationProcessor;
return this;
}
public Builder messageCallProcessor(final MessageCallProcessor messageCallProcessor) {
this.messageCallProcessor = messageCallProcessor;
return this;
}
public Builder clearEmptyAccounts(final boolean clearEmptyAccounts) {
this.clearEmptyAccounts = clearEmptyAccounts;
return this;
}
public Builder warmCoinbase(final boolean warmCoinbase) {
this.warmCoinbase = warmCoinbase;
return this;
}
public Builder maxStackSize(final int maxStackSize) {
this.maxStackSize = maxStackSize;
return this;
}
public Builder feeMarket(final FeeMarket feeMarket) {
this.feeMarket = feeMarket;
return this;
}
public Builder coinbaseFeePriceCalculator(
final CoinbaseFeePriceCalculator coinbaseFeePriceCalculator) {
this.coinbaseFeePriceCalculator = coinbaseFeePriceCalculator;
return this;
}
public Builder codeDelegationProcessor(
final CodeDelegationProcessor maybeCodeDelegationProcessor) {
this.codeDelegationProcessor = maybeCodeDelegationProcessor;
return this;
}
public Builder transferLogEmitter(final TransferLogEmitter transferLogEmitter) {
this.transferLogEmitter = transferLogEmitter;
return this;
}
public Builder populateFrom(final MainnetTransactionProcessor processor) {
this.gasCalculator = processor.gasCalculator;
this.transactionValidatorFactory = processor.transactionValidatorFactory;
this.contractCreationProcessor = processor.contractCreationProcessor;
this.messageCallProcessor = processor.messageCallProcessor;
this.clearEmptyAccounts = processor.clearEmptyAccounts;
this.warmCoinbase = processor.warmCoinbase;
this.maxStackSize = processor.maxStackSize;
this.feeMarket = processor.feeMarket;
this.coinbaseFeePriceCalculator = processor.coinbaseFeePriceCalculator;
this.codeDelegationProcessor = processor.maybeCodeDelegationProcessor.orElse(null);
this.transferLogEmitter = processor.transferLogEmitter;
return this;
}
public MainnetTransactionProcessor build() {
return new MainnetTransactionProcessor(
gasCalculator,
transactionValidatorFactory,
contractCreationProcessor,
messageCallProcessor,
clearEmptyAccounts,
warmCoinbase,
maxStackSize,
feeMarket,
coinbaseFeePriceCalculator,
codeDelegationProcessor,
transferLogEmitter);
}
}
}

View File

@ -0,0 +1,395 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-tx-gas-cap-unsigned.patch.
*/
package org.hyperledger.besu.ethereum.mainnet;
import static org.hyperledger.besu.evm.account.Account.MAX_NONCE;
import static org.hyperledger.besu.evm.internal.Words.clampedAdd;
import static org.hyperledger.besu.evm.worldstate.CodeDelegationHelper.hasCodeDelegation;
import org.hyperledger.besu.crypto.SECPSignature;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.BlobType;
import org.hyperledger.besu.datatypes.CodeDelegation;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.GasLimitCalculator;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket;
import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason;
import org.hyperledger.besu.evm.account.Account;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;
import java.math.BigInteger;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Validates a transaction based on Frontier protocol runtime requirements.
*
* <p>The {@link MainnetTransactionValidator} performs the intrinsic gas cost check on the given
* {@link Transaction}.
*/
public class MainnetTransactionValidator implements TransactionValidator {
private static final Logger LOG = LoggerFactory.getLogger(MainnetTransactionValidator.class);
public static final BigInteger TWO_POW_256 = BigInteger.TWO.pow(256);
private final GasCalculator gasCalculator;
private final GasLimitCalculator gasLimitCalculator;
private final FeeMarket feeMarket;
private final boolean disallowSignatureMalleability;
private final Optional<BigInteger> chainId;
private final Set<TransactionType> acceptedTransactionTypes;
private final int maxInitcodeSize;
private final MainnetBlobsValidator blobsValidator;
public MainnetTransactionValidator(
final GasCalculator gasCalculator,
final GasLimitCalculator gasLimitCalculator,
final FeeMarket feeMarket,
final boolean checkSignatureMalleability,
final Optional<BigInteger> chainId,
final Set<TransactionType> acceptedTransactionTypes,
final Set<BlobType> acceptedBlobVersions,
final int maxInitcodeSize) {
this.gasCalculator = gasCalculator;
this.gasLimitCalculator = gasLimitCalculator;
this.feeMarket = feeMarket;
this.disallowSignatureMalleability = checkSignatureMalleability;
this.chainId = chainId;
this.acceptedTransactionTypes = acceptedTransactionTypes;
this.maxInitcodeSize = maxInitcodeSize;
this.blobsValidator =
new MainnetBlobsValidator(acceptedBlobVersions, gasLimitCalculator, gasCalculator);
}
@Override
public ValidationResult<TransactionInvalidReason> validate(
final Transaction transaction,
final Optional<Wei> baseFee,
final Optional<Wei> blobFee,
final TransactionValidationParams transactionValidationParams) {
final ValidationResult<TransactionInvalidReason> signatureResult =
validateTransactionSignature(transaction);
if (!signatureResult.isValid()) {
return signatureResult;
}
final TransactionType transactionType = transaction.getType();
if (!acceptedTransactionTypes.contains(transactionType)) {
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_TRANSACTION_FORMAT,
String.format(
"Transaction type %s is invalid, accepted transaction types are %s",
transactionType, acceptedTransactionTypes));
}
if (!transactionValidationParams.isAllowFutureNonce() && transaction.getNonce() == MAX_NONCE) {
return ValidationResult.invalid(
TransactionInvalidReason.NONCE_OVERFLOW, "Nonce must be less than 2^64-1");
}
if (!transactionValidationParams.isAllowExceedingGasLimit()
// AERE D-432: unsigned, like the intrinsic-gas check below. The decoder reads gasLimit with readLongScalar, so a limit of 2^63
// or more arrives as a negative long, and a signed compare let it through the EIP-7825 cap.
&& Long.compareUnsigned(transaction.getGasLimit(), gasLimitCalculator.transactionGasLimitCap()) > 0) {
return ValidationResult.invalid(
TransactionInvalidReason.EXCEEDS_TRANSACTION_GAS_LIMIT,
"Transaction gas limit must be at most " + gasLimitCalculator.transactionGasLimitCap());
}
if (transactionType.supportsBlob()) {
final ValidationResult<TransactionInvalidReason> blobTransactionResult =
blobsValidator.validate(transaction);
if (!blobTransactionResult.isValid()) {
LOG.debug(
"Blob transaction {} validation failed: {}",
transaction.getHash().getBytes().toHexString(),
blobTransactionResult.getErrorMessage());
return blobTransactionResult;
}
}
if (transaction.isContractCreation() && transaction.getPayload().size() > maxInitcodeSize) {
return ValidationResult.invalid(
TransactionInvalidReason.INITCODE_TOO_LARGE,
String.format(
"Initcode size of %d exceeds maximum size of %s",
transaction.getPayload().size(), maxInitcodeSize));
}
if (transactionType == TransactionType.DELEGATE_CODE) {
ValidationResult<TransactionInvalidReason> codeDelegationValidation =
validateCodeDelegation(transaction);
if (!codeDelegationValidation.isValid()) {
return codeDelegationValidation;
}
}
return validateCostAndFee(transaction, baseFee, blobFee, transactionValidationParams);
}
private static ValidationResult<TransactionInvalidReason> validateCodeDelegation(
final Transaction transaction) {
if (isDelegateCodeEmpty(transaction)) {
return ValidationResult.invalid(
TransactionInvalidReason.EMPTY_CODE_DELEGATION,
"transaction code delegation transactions must have a non-empty code delegation list");
}
if (transaction.getTo().isEmpty()) {
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_TRANSACTION_FORMAT,
"transaction code delegation transactions must have a to address");
}
final Optional<ValidationResult<TransactionInvalidReason>> validationResult =
transaction
.getCodeDelegationList()
.map(
codeDelegations -> {
for (CodeDelegation codeDelegation : codeDelegations) {
if (codeDelegation.chainId().compareTo(TWO_POW_256) >= 0) {
throw new IllegalArgumentException(
"Invalid 'chainId' value, should be < 2^256 but got "
+ codeDelegation.chainId());
}
if (codeDelegation.r().compareTo(TWO_POW_256) >= 0) {
throw new IllegalArgumentException(
"Invalid 'r' value, should be < 2^256 but got " + codeDelegation.r());
}
if (codeDelegation.s().compareTo(TWO_POW_256) >= 0) {
throw new IllegalArgumentException(
"Invalid 's' value, should be < 2^256 but got " + codeDelegation.s());
}
}
return ValidationResult.valid();
});
if (validationResult.isPresent() && !validationResult.get().isValid()) {
return validationResult.get();
}
return ValidationResult.valid();
}
private static boolean isDelegateCodeEmpty(final Transaction transaction) {
return transaction.getCodeDelegationList().isEmpty()
|| transaction.getCodeDelegationList().get().isEmpty();
}
private ValidationResult<TransactionInvalidReason> validateCostAndFee(
final Transaction transaction,
final Optional<Wei> maybeBaseFee,
final Optional<Wei> maybeBlobFee,
final TransactionValidationParams transactionValidationParams) {
if (maybeBaseFee.isPresent()) {
final Wei price = feeMarket.getTransactionPriceCalculator().price(transaction, maybeBaseFee);
if (!transactionValidationParams.allowUnderpriced()
&& !transactionValidationParams.isPreserveCallerGasPricing()
&& price.compareTo(maybeBaseFee.orElseThrow()) < 0) {
return ValidationResult.invalid(
TransactionInvalidReason.GAS_PRICE_BELOW_CURRENT_BASE_FEE,
"gasPrice is less than the current BaseFee");
}
// assert transaction.max_fee_per_gas >= transaction.max_priority_fee_per_gas
if (transaction.getType().supports1559FeeMarket()
&& transaction
.getMaxPriorityFeePerGas()
.get()
.getAsBigInteger()
.compareTo(transaction.getMaxFeePerGas().get().getAsBigInteger())
> 0) {
return ValidationResult.invalid(
TransactionInvalidReason.MAX_PRIORITY_FEE_PER_GAS_EXCEEDS_MAX_FEE_PER_GAS,
"max priority fee per gas cannot be greater than max fee per gas");
}
}
if (transaction.getType().supportsBlob()) {
final long txTotalBlobGas = gasCalculator.blobGasCost(transaction.getBlobCount());
if (txTotalBlobGas > gasLimitCalculator.currentBlobGasLimit()) {
return ValidationResult.invalid(
TransactionInvalidReason.TOTAL_BLOB_GAS_TOO_HIGH,
String.format(
"total blob gas %d exceeds max blob gas per block %d",
txTotalBlobGas, gasLimitCalculator.currentBlobGasLimit()));
}
if (maybeBlobFee.isEmpty()) {
throw new IllegalArgumentException(
"blob fee must be provided from blocks containing blobs");
// tx.getMaxFeePerBlobGas can be empty for eth_call
} else if (!transactionValidationParams.allowUnderpriced()
&& maybeBlobFee.get().compareTo(transaction.getMaxFeePerBlobGas().get()) > 0) {
return ValidationResult.invalid(
TransactionInvalidReason.BLOB_GAS_PRICE_BELOW_CURRENT_BLOB_BASE_FEE,
String.format(
"tx max fee per blob gas less than block blob gas fee: address %s blobGasFeeCap: %s, blobBaseFee: %s",
transaction.getSender().getBytes().toHexString(),
transaction.getMaxFeePerBlobGas().get().toHumanReadableString(),
maybeBlobFee.get().toHumanReadableString()));
}
}
final long baselineGas =
clampedAdd(
transaction.getAccessList().map(gasCalculator::accessListGasCost).orElse(0L),
gasCalculator.delegateCodeGasCost(transaction.codeDelegationListSize()));
final long intrinsicGasCostOrFloor =
Math.max(
gasCalculator.transactionIntrinsicGasCost(transaction, baselineGas),
gasCalculator.transactionFloorCost(
transaction.getPayload(), transaction.getPayloadZeroBytes()));
if (Long.compareUnsigned(intrinsicGasCostOrFloor, transaction.getGasLimit()) > 0) {
return ValidationResult.invalid(
TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT,
String.format(
"intrinsic gas cost %s exceeds gas limit %s",
intrinsicGasCostOrFloor, transaction.getGasLimit()));
}
if (transaction.calculateUpfrontGasCost(transaction.getMaxGasPrice(), Wei.ZERO, 0).bitLength()
> 256) {
return ValidationResult.invalid(
TransactionInvalidReason.UPFRONT_COST_EXCEEDS_UINT256,
"Upfront gas cost cannot exceed 2^256 Wei");
}
return ValidationResult.valid();
}
@Override
public ValidationResult<TransactionInvalidReason> validateForSender(
final Transaction transaction,
final Account sender,
final TransactionValidationParams validationParams) {
Wei senderBalance = Account.DEFAULT_BALANCE;
long senderNonce = Account.DEFAULT_NONCE;
Hash codeHash = Hash.EMPTY;
if (sender != null) {
senderBalance = sender.getBalance();
senderNonce = sender.getNonce();
if (sender.getCodeHash() != null) codeHash = sender.getCodeHash();
}
final Wei upfrontCost =
transaction.getUpfrontCost(gasCalculator.blobGasCost(transaction.getBlobCount()));
if (!validationParams.allowUnderpriced() && upfrontCost.compareTo(senderBalance) > 0) {
return ValidationResult.invalid(
TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE,
String.format(
"transaction up-front cost %s exceeds transaction sender account balance %s for sender %s",
upfrontCost.toQuantityHexString(),
senderBalance.toQuantityHexString(),
transaction.getSender()));
}
if (Long.compareUnsigned(transaction.getNonce(), senderNonce) < 0) {
return ValidationResult.invalid(
TransactionInvalidReason.NONCE_TOO_LOW,
String.format(
"transaction nonce %s below sender account nonce %s for sender %s",
transaction.getNonce(), senderNonce, transaction.getSender()));
}
if (!validationParams.isAllowFutureNonce() && senderNonce != transaction.getNonce()) {
return ValidationResult.invalid(
TransactionInvalidReason.NONCE_TOO_HIGH,
String.format(
"transaction nonce %s does not match sender account nonce %s for sender %s",
transaction.getNonce(), senderNonce, transaction.getSender()));
}
if (!validationParams.isAllowContractAddressAsSender()
&& !canSendTransaction(sender, codeHash)) {
return ValidationResult.invalid(
TransactionInvalidReason.TX_SENDER_NOT_AUTHORIZED,
String.format(
"Sender %s has deployed code and so is not authorized to send transactions",
transaction.getSender()));
}
return ValidationResult.valid();
}
private static boolean canSendTransaction(final Account sender, final Hash codeHash) {
return codeHash.equals(Hash.EMPTY) || hasCodeDelegation(sender.getCode());
}
private ValidationResult<TransactionInvalidReason> validateTransactionSignature(
final Transaction transaction) {
if (chainId.isPresent()
&& (transaction.getChainId().isPresent() && !transaction.getChainId().equals(chainId))) {
return ValidationResult.invalid(
TransactionInvalidReason.WRONG_CHAIN_ID,
String.format(
"transaction was meant for chain id %s and not this chain id %s",
transaction.getChainId().get(), chainId.get()));
}
if (chainId.isEmpty() && transaction.getChainId().isPresent()) {
return ValidationResult.invalid(
TransactionInvalidReason.REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED,
"replay protected signatures is not supported");
}
if (transaction.getType().supportsPqAuthorization()) {
// AIP-20: no secp256k1 material exists; the ML-DSA authorization is what is verified.
return AerePqTransactionSupport.validateAuthorization(transaction);
}
final SECPSignature signature = transaction.getSignature();
final BigInteger halfCurveOrder = SignatureAlgorithmFactory.getInstance().getHalfCurveOrder();
if (disallowSignatureMalleability && signature.getS().compareTo(halfCurveOrder) > 0) {
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_SIGNATURE,
String.format(
"Signature s value should be less than %s, but got %s",
halfCurveOrder, signature.getS()));
}
// org.bouncycastle.math.ec.ECCurve.AbstractFp.decompressPoint throws an
// IllegalArgumentException for "Invalid point compression" for bad signatures.
try {
transaction.getSender();
} catch (final IllegalArgumentException e) {
return ValidationResult.invalid(
TransactionInvalidReason.INVALID_SIGNATURE,
"sender could not be extracted from transaction signature");
}
return ValidationResult.valid();
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-bft-withdrawals-not-applicable.patch.
*/
package org.hyperledger.besu.ethereum.mainnet;
import static com.google.common.base.Preconditions.checkArgument;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.Withdrawal;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public interface WithdrawalsValidator {
boolean validateWithdrawals(Optional<List<Withdrawal>> withdrawals);
boolean validateWithdrawalsRoot(Block block);
class ProhibitedWithdrawals implements WithdrawalsValidator {
private static final Logger LOG = LoggerFactory.getLogger(ProhibitedWithdrawals.class);
@Override
public boolean validateWithdrawals(final Optional<List<Withdrawal>> withdrawals) {
final boolean isValid = withdrawals.isEmpty();
if (!isValid) {
LOG.warn(
"withdrawals must be null when Withdrawals are prohibited but were: {}", withdrawals);
}
return isValid;
}
@Override
public boolean validateWithdrawalsRoot(final Block block) {
final Optional<Hash> withdrawalsRoot = block.getHeader().getWithdrawalsRoot();
if (withdrawalsRoot.isPresent()) {
LOG.warn(
"withdrawalsRoot must be null when Withdrawals are prohibited but was: {}",
withdrawalsRoot.get());
return false;
}
return true;
}
}
class AllowedWithdrawals implements WithdrawalsValidator {
private static final Logger LOG = LoggerFactory.getLogger(AllowedWithdrawals.class);
@Override
public boolean validateWithdrawals(final Optional<List<Withdrawal>> withdrawals) {
final boolean isValid = withdrawals.isPresent();
if (!isValid) {
LOG.warn("withdrawals must not be null when Withdrawals are activated");
}
return isValid;
}
@Override
public boolean validateWithdrawalsRoot(final Block block) {
checkArgument(
block.getBody().getWithdrawals().isPresent(), "Block body must contain withdrawals");
final Optional<Hash> withdrawalsRoot = block.getHeader().getWithdrawalsRoot();
if (withdrawalsRoot.isEmpty()) {
LOG.warn("withdrawalsRoot must not be null when Withdrawals are activated");
return false;
}
final List<Withdrawal> withdrawals = block.getBody().getWithdrawals().get();
final Hash expectedWithdrawalsRoot = BodyValidation.withdrawalsRoot(withdrawals);
if (!expectedWithdrawalsRoot.equals(withdrawalsRoot.get())) {
LOG.info(
"Invalid block: transaction root mismatch (expected={}, actual={})",
expectedWithdrawalsRoot,
withdrawalsRoot.get());
return false;
}
return true;
}
}
class NotApplicableWithdrawals implements WithdrawalsValidator {
private static final Logger LOG = LoggerFactory.getLogger(NotApplicableWithdrawals.class);
// AERE D-433: ported from upstream hyperledger/besu ffe8104ee2 (GHSA-p4h2-gvh4-pv6j) and b7f1387cb4. On a BFT chain a non-empty list is a
// Byzantine proposer's injection that WithdrawalsProcessor would credit as native balance on every honest node.
@Override
public boolean validateWithdrawals(final Optional<List<Withdrawal>> withdrawals) {
// Pre-Shanghai blocks have no withdrawals field (Optional.empty()) - valid.
// Shanghai+ blocks must carry an empty list; a non-empty list means a Byzantine
// proposer injected unauthorized withdrawals that would be executed as balance credits.
if (withdrawals.isPresent() && !withdrawals.get().isEmpty()) {
LOG.warn(
"Withdrawals not applicable but block contained {} withdrawal(s)",
withdrawals.get().size());
return false;
}
return true;
}
@Override
public boolean validateWithdrawalsRoot(final Block block) {
final Optional<Hash> withdrawalsRoot = block.getHeader().getWithdrawalsRoot();
if (withdrawalsRoot.isEmpty()) {
return true; // pre-Shanghai block, no withdrawals root field
}
// A block with a withdrawalsRoot in the header but no withdrawals field in the body is
// malformed.
if (block.getBody().getWithdrawals().isEmpty()) {
LOG.warn(
"Invalid block: withdrawalsRoot present in header but withdrawals absent from body");
return false;
}
if (!Hash.EMPTY_TRIE_HASH.equals(withdrawalsRoot.get())) {
LOG.warn(
"Invalid block: withdrawals root mismatch (expected={}, actual={})",
Hash.EMPTY_TRIE_HASH,
withdrawalsRoot.get());
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,528 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.MutableWorldState;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor;
import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams;
import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BlockAccessListBuilder;
import org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockTransactionProcessor;
import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult;
import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldState;
import org.hyperledger.besu.evm.account.Account;
import org.hyperledger.besu.evm.account.MutableAccount;
import org.hyperledger.besu.evm.blockhash.BlockHashLookup;
import org.hyperledger.besu.evm.tracing.OperationTracer;
import org.hyperledger.besu.evm.worldstate.WorldUpdater;
import org.hyperledger.besu.plugin.services.metrics.Counter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* AERE Block-STM optimistic parallel execution engine, wired into Besu's block-processing
* preprocessing hook. Faithful realisation of the AERE Rust Block-STM (Gelashvili et al. 2022):
* {@link Scheduler} + {@link MvMemory} drive a worker pool that runs the block's transactions
* concurrently against a multi-version world view ({@link MvBaseLayer}/{@link MvAccount}), captures
* read/write sets, re-executes conflicting transactions, and yields per-transaction outcomes that
* the block processor commits IN BLOCK ORDER — producing a state root bit-identical to sequential
* execution.
*
* <p>Safety: any transaction whose parallel outcome is missing, unsuccessful, or produced by a run
* that did not reach {@code scheduler.done()} falls back to Besu's sequential execution in the block
* loop (see {@link #getProcessingResult}). The engine can never commit an unverified result.
*/
@SuppressWarnings("MissingSummary")
public class AereBlockStmEngine extends ParallelBlockTransactionProcessor {
private static final Logger LOG = LoggerFactory.getLogger(AereBlockStmEngine.class);
private static final long PARALLEL_TIMEOUT_SECONDS = 60;
private final MainnetTransactionProcessor transactionProcessor;
private final int numThreads;
// Populated by runAsyncBlock, consumed in block order by getProcessingResult.
private volatile MvOutcome[] outcomes;
private volatile boolean success = false;
private final AtomicInteger totalExecutions = new AtomicInteger();
private final AtomicInteger totalValidations = new AtomicInteger();
private final AtomicInteger totalAborts = new AtomicInteger();
private final AtomicInteger committedParallel = new AtomicInteger();
// Execution-phase instrumentation (see BlockStmStats): snapshotNanos = time to mint the single
// parent Bonsai view + flat pre-snapshot wrapper; execPhaseNanos = the PARALLEL EXECUTION PHASE
// (worker pool start -> termination) in isolation. Neither includes the downstream serial Bonsai
// trie commit, which happens in the block processor after this preprocessing returns.
private volatile long snapshotNanos = 0L;
private volatile long execPhaseNanos = 0L;
/**
* @param transactionProcessor the mainnet transaction processor (EVM)
* @param numThreads worker-pool size (typically NCPU)
*/
public AereBlockStmEngine(
final MainnetTransactionProcessor transactionProcessor, final int numThreads) {
this.transactionProcessor = transactionProcessor;
this.numThreads = Math.max(1, numThreads);
}
/** Per-transaction net outcome captured by the parallel phase. */
private static final class MvOutcome {
final Map<MvKey, Object> writes;
final Set<MvKey> writeKeys;
final List<Map.Entry<Address, Map<UInt256, UInt256>>> storageByAddr;
final Map<Address, long[]> nonceByAddr; // addr -> [nonce] if written
final Map<Address, Wei> balanceByAddr; // addr -> balance if written
final Map<Address, Bytes> codeByAddr; // addr -> code if written
final Set<Address> deleted;
Wei beneficiaryDelta = Wei.ZERO;
TransactionProcessingResult result;
MvOutcome() {
this.writes = new HashMap<>();
this.writeKeys = new HashSet<>();
this.storageByAddr = new ArrayList<>();
this.nonceByAddr = new HashMap<>();
this.balanceByAddr = new HashMap<>();
this.codeByAddr = new HashMap<>();
this.deleted = new HashSet<>();
}
}
// ---- shared per-block execution state ----
private MvMemory mv;
private Scheduler scheduler;
private List<Transaction> txns;
private BlockHeader blockHeader;
private Address beneficiary;
private BlockHashLookup blockHashLookup;
private Wei blobGasPrice;
private Object[] slotLocks;
private Map<MvKey, MvMemory.ReadDescriptor>[] lastReads;
private Set<MvKey>[] lastWriteKeys;
private final AtomicBoolean hardFail = new AtomicBoolean(false);
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public void runAsyncBlock(
final ProtocolContext protocolContext,
final BlockHeader header,
final List<Transaction> transactions,
final Address miningBeneficiary,
final BlockHashLookup blockHashLookupParam,
final Wei blobGasPriceParam,
final java.util.concurrent.Executor executorUnused,
final Optional<BlockAccessListBuilder> blockAccessListBuilder,
final Optional<BlockHeader> maybeParentHeader) {
final int n = transactions.size();
outcomes = new MvOutcome[n];
success = false;
if (n == 0 || maybeParentHeader.isEmpty()) {
return;
}
// AERE Block-STM Track B: FLAT immutable base pre-snapshot shared lock-free by all workers.
// Iteration 1 gave each worker its OWN frozen BonsaiWorldState and re-walked the parent trie on
// every base read (task bottleneck #2: per-worker Bonsai-view minting + layered-trie traversal
// per read). Here we mint the parent Bonsai view exactly ONCE, wrap it in a FlatBaseSnapshot
// that materialises each base value ONCE into an in-memory ConcurrentHashMap, and hand the SAME
// snapshot to every worker. Hot-path base reads become lock-free O(1) hashmap lookups with no
// trie traversal and no per-worker view. The base state is immutable for the round, so all
// workers observe identical parent values and serial equivalence holds (parallelRoot ==
// sequentialRoot). snapshotNanos captures this one-time mint separately from execPhaseNanos.
final long snapT0 = System.nanoTime();
final List<BonsaiWorldState> openStates = new ArrayList<>(1);
try {
final BonsaiWorldState ws =
getWorldState(protocolContext, maybeParentHeader.get()).orElse(null);
if (ws == null) {
return; // success stays false -> sequential fallback
}
ws.disableCacheMerkleTrieLoader();
openStates.add(ws);
final BaseReader sharedBase = new FlatBaseSnapshot(ws);
this.snapshotNanos = System.nanoTime() - snapT0;
this.mv = new MvMemory();
this.scheduler = new Scheduler(n);
this.txns = transactions;
this.blockHeader = header;
this.beneficiary = miningBeneficiary;
this.blockHashLookup = blockHashLookupParam;
this.blobGasPrice = blobGasPriceParam;
this.slotLocks = new Object[n];
this.lastReads = new Map[n];
this.lastWriteKeys = new Set[n];
for (int i = 0; i < n; i++) {
slotLocks[i] = new Object();
lastWriteKeys[i] = new HashSet<>();
}
hardFail.set(false);
// EXECUTION PHASE (timed in isolation from the snapshot mint above and from the downstream
// serial Bonsai trie commit that runs after this method returns). All workers share the one
// FlatBaseSnapshot; its hot-path reads are lock-free, so distinct-key base reads never contend.
final long execT0 = System.nanoTime();
final ExecutorService pool = Executors.newFixedThreadPool(numThreads);
try {
for (int t = 0; t < numThreads; t++) {
pool.execute(() -> worker(sharedBase));
}
pool.shutdown();
final boolean finished = pool.awaitTermination(PARALLEL_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!finished) {
pool.shutdownNow();
}
this.execPhaseNanos = System.nanoTime() - execT0;
success = finished && scheduler.done() && !hardFail.get();
} catch (final InterruptedException ie) {
Thread.currentThread().interrupt();
pool.shutdownNow();
this.execPhaseNanos = System.nanoTime() - execT0;
success = false;
}
if (!success) {
LOG.info(
"AERE Block-STM: parallel phase did not verify (done={}, hardFail={}); block falls back"
+ " to sequential per-transaction execution",
scheduler.done(),
hardFail.get());
}
} catch (final RuntimeException ex) {
LOG.info("AERE Block-STM: parallel preprocessing error; sequential fallback", ex);
success = false;
} finally {
for (final BonsaiWorldState ws : openStates) {
try {
ws.close();
} catch (final Exception ignored) {
// best-effort close
}
}
}
}
private void worker(final BaseReader base) {
try {
Scheduler.Task task = scheduler.nextTask();
while (!hardFail.get()) {
switch (task.kind()) {
case DONE:
return;
case NO_TASK:
task = scheduler.nextTask();
break;
case EXECUTION:
task = tryExecute(base, task.idx(), task.inc());
break;
case VALIDATION:
task = tryValidate(task.idx(), task.inc());
break;
default:
return;
}
}
} catch (final Throwable th) {
hardFail.set(true);
LOG.debug("AERE Block-STM worker aborted", th);
}
}
private Scheduler.Task tryExecute(final BaseReader base, final int idx, final int inc) {
while (true) {
if (hardFail.get()) {
return Scheduler.Task.NO_TASK;
}
final MvContext ctx = new MvContext(idx, mv, base, beneficiary);
final MvBaseLayer baseLayer = new MvBaseLayer(ctx);
final WorldUpdater txUpdater = baseLayer.updater();
totalExecutions.incrementAndGet();
TransactionProcessingResult res = null;
try {
res =
transactionProcessor.processTransaction(
txUpdater,
blockHeader,
txns.get(idx).detachedCopy(),
beneficiary,
OperationTracer.NO_TRACING,
blockHashLookup,
TransactionValidationParams.processingBlock(),
blobGasPrice,
Optional.empty());
} catch (final RuntimeException ex) {
if (ctx.blockedOn < 0) {
throw ex; // genuine failure -> worker aborts -> sequential fallback
}
// else: a routed read parked; fall through to dependency handling
}
if (ctx.blockedOn >= 0) {
final int blocking = ctx.blockedOn;
if (scheduler.addDependency(idx, blocking)) {
return Scheduler.Task.NO_TASK; // parked
}
continue; // dependency already resolved; re-run same incarnation
}
final MvOutcome outcome = harvest(ctx, baseLayer, txUpdater, res);
final Set<MvKey> prevKeys;
synchronized (slotLocks[idx]) {
prevKeys = lastWriteKeys[idx];
}
final boolean wroteNewPath = mv.applyWriteSet(idx, inc, outcome.writes, prevKeys);
synchronized (slotLocks[idx]) {
lastReads[idx] = ctx.reads;
lastWriteKeys[idx] = outcome.writeKeys;
outcomes[idx] = outcome;
}
return scheduler.finishExecution(idx, inc, wroteNewPath);
}
}
private Scheduler.Task tryValidate(final int idx, final int inc) {
totalValidations.incrementAndGet();
final Map<MvKey, MvMemory.ReadDescriptor> reads;
synchronized (slotLocks[idx]) {
reads = lastReads[idx];
}
boolean valid = true;
if (reads != null) {
for (final Map.Entry<MvKey, MvMemory.ReadDescriptor> e : reads.entrySet()) {
final MvMemory.ReadResult now = mv.read(e.getKey(), idx);
final boolean matches = !now.isBlocked() && e.getValue().sameProvenance(now.descriptor());
if (!matches) {
valid = false;
break;
}
}
}
final boolean aborted = !valid && scheduler.tryValidationAbort(idx, inc);
if (aborted) {
totalAborts.incrementAndGet();
final Set<MvKey> keys;
synchronized (slotLocks[idx]) {
keys = lastWriteKeys[idx];
}
if (keys != null) {
mv.markEstimates(idx, keys);
}
}
return scheduler.finishValidation(idx, aborted);
}
private MvOutcome harvest(
final MvContext ctx,
final MvBaseLayer baseLayer,
final WorldUpdater txUpdater,
final TransactionProcessingResult res) {
final MvOutcome o = new MvOutcome();
o.result = res;
for (final Account acct : txUpdater.getTouchedAccounts()) {
final Address a = acct.getAddress();
final MvAccount seed = baseLayer.materializedAccount(a);
final long seedNonce = seed == null ? 0L : seed.seededNonce();
final Wei seedBalance = seed == null ? Wei.ZERO : seed.seededBalance();
final Hash seedCodeHash = seed == null ? Hash.EMPTY : seed.seededCodeHash();
final long nonce = acct.getNonce();
if (nonce != seedNonce) {
o.nonceByAddr.put(a, new long[] {nonce});
final MvKey k = MvKey.nonce(a);
o.writes.put(k, Long.valueOf(nonce));
o.writeKeys.add(k);
}
if (ctx.isBeneficiary(a)) {
final Wei bal = acct.getBalance();
o.beneficiaryDelta = bal.subtract(seedBalance);
} else {
final Wei bal = acct.getBalance();
if (!bal.equals(seedBalance)) {
o.balanceByAddr.put(a, bal);
final MvKey k = MvKey.balance(a);
o.writes.put(k, bal);
o.writeKeys.add(k);
}
}
final Hash codeHash = acct.getCodeHash();
if (!codeHash.equals(seedCodeHash)) {
final Bytes code = acct.getCode();
o.codeByAddr.put(a, code);
final MvKey k = MvKey.code(a);
o.writes.put(k, code);
o.writeKeys.add(k);
}
final Map<UInt256, UInt256> updated =
(acct instanceof MutableAccount) ? ((MutableAccount) acct).getUpdatedStorage() : null;
if (updated != null && !updated.isEmpty()) {
final Map<UInt256, UInt256> copy = new HashMap<>(updated);
o.storageByAddr.add(Map.entry(a, copy));
for (final Map.Entry<UInt256, UInt256> se : copy.entrySet()) {
final MvKey k = MvKey.storage(a, se.getKey());
o.writes.put(k, se.getValue());
o.writeKeys.add(k);
}
}
}
for (final Address d : txUpdater.getDeletedAccountAddresses()) {
o.deleted.add(d);
// reflect clearing in MVMemory so higher transactions read cleared state
final MvKey bk = MvKey.balance(d);
final MvKey nk = MvKey.nonce(d);
final MvKey ck = MvKey.code(d);
o.writes.put(bk, Wei.ZERO);
o.writes.put(nk, Long.valueOf(0L));
o.writes.put(ck, Bytes.EMPTY);
o.writeKeys.add(bk);
o.writeKeys.add(nk);
o.writeKeys.add(ck);
}
return o;
}
/**
* Commit phase: called by the block processor in strict block order (location 0..n-1) on the real
* world state. Applies transaction {@code location}'s parallel outcome and returns its result, or
* empty to request sequential re-execution for this transaction.
*/
@Override
public Optional<TransactionProcessingResult> getProcessingResult(
final MutableWorldState worldState,
final Address miningBeneficiary,
final Transaction transaction,
final int location,
final Optional<Counter> confirmedParallelizedTransactionCounter,
final Optional<Counter> conflictingButCachedTransactionCounter) {
if (!success || outcomes == null || location >= outcomes.length) {
return Optional.empty();
}
final MvOutcome o = outcomes[location];
if (o == null || o.result == null || !o.result.isSuccessful()) {
return Optional.empty();
}
try {
final WorldUpdater updater = worldState.updater();
// deletions win over writes for the same address
for (final Map.Entry<Address, Wei> e : o.balanceByAddr.entrySet()) {
if (!o.deleted.contains(e.getKey())) {
updater.getOrCreate(e.getKey()).setBalance(e.getValue());
}
}
for (final Map.Entry<Address, long[]> e : o.nonceByAddr.entrySet()) {
if (!o.deleted.contains(e.getKey())) {
updater.getOrCreate(e.getKey()).setNonce(e.getValue()[0]);
}
}
for (final Map.Entry<Address, Bytes> e : o.codeByAddr.entrySet()) {
if (!o.deleted.contains(e.getKey())) {
updater.getOrCreate(e.getKey()).setCode(e.getValue());
}
}
for (final Map.Entry<Address, Map<UInt256, UInt256>> e : o.storageByAddr) {
if (o.deleted.contains(e.getKey())) {
continue;
}
final MutableAccount ra = updater.getOrCreate(e.getKey());
for (final Map.Entry<UInt256, UInt256> se : e.getValue().entrySet()) {
ra.setStorageValue(se.getKey(), se.getValue());
}
}
if (!o.beneficiaryDelta.isZero()) {
updater.getOrCreate(miningBeneficiary).incrementBalance(o.beneficiaryDelta);
}
for (final Address d : o.deleted) {
updater.deleteAccount(d);
}
updater.commit();
committedParallel.incrementAndGet();
confirmedParallelizedTransactionCounter.ifPresent(Counter::inc);
o.result.setIsProcessedInParallel(Optional.of(Boolean.TRUE));
return Optional.of(o.result);
} catch (final RuntimeException ex) {
LOG.info("AERE Block-STM: commit of tx {} failed; sequential fallback", location, ex);
return Optional.empty();
}
}
@Override
protected org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelizedTransactionContext
runTransaction(
final ProtocolContext protocolContext,
final BlockHeader header,
final int transactionLocation,
final Transaction transaction,
final Address miningBeneficiary,
final BlockHashLookup blockHashLookupParam,
final Wei blobGasPriceParam,
final Optional<BlockAccessListBuilder> blockAccessListBuilder,
final Optional<BlockHeader> maybeParentHeader) {
// Unused: AERE Block-STM overrides runAsyncBlock wholesale.
return null;
}
/** @return whether the last parallel run verified (reached done with no hard failure). */
public boolean lastRunSucceeded() {
return success;
}
/** @return Block-STM statistics for the last run. */
public BlockStmStats stats() {
return new BlockStmStats(
outcomes == null ? 0 : outcomes.length,
numThreads,
totalExecutions.get(),
totalValidations.get(),
totalAborts.get(),
committedParallel.get(),
success,
snapshotNanos,
execPhaseNanos);
}
/** @return wall-clock nanos of the last parallel EXECUTION PHASE (worker pool only). */
public long execPhaseNanos() {
return execPhaseNanos;
}
/** @return wall-clock nanos spent building the last flat base pre-snapshot. */
public long snapshotNanos() {
return snapshotNanos;
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.AbstractBlockProcessor.PreprocessingFunction;
import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor;
import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList;
import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList.BlockAccessListBuilder;
import org.hyperledger.besu.ethereum.mainnet.parallelization.PreprocessingContext;
import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.PathBasedWorldStateProvider;
import org.hyperledger.besu.evm.blockhash.BlockHashLookup;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Preprocessing hook that runs AERE Block-STM optimistic parallel execution over the block's
* transactions before the sequential commit loop. Occupies the same {@code PreprocessingFunction}
* slot Besu uses for its built-in single-round parallelizer, but drives the AERE multi-version
* Block-STM engine instead. A fresh {@link AereBlockStmEngine} is created per block, so no state
* leaks between blocks.
*/
@SuppressWarnings("MissingSummary")
public class AereBlockStmPreprocessing implements PreprocessingFunction {
private static final Logger LOG = LoggerFactory.getLogger(AereBlockStmPreprocessing.class);
private final MainnetTransactionProcessor transactionProcessor;
private final int numThreads;
/**
* @param transactionProcessor the EVM transaction processor
* @param numThreads worker-pool size (typically NCPU)
*/
public AereBlockStmPreprocessing(
final MainnetTransactionProcessor transactionProcessor, final int numThreads) {
this.transactionProcessor = transactionProcessor;
this.numThreads = numThreads;
}
@Override
public Optional<PreprocessingContext> run(
final ProtocolContext protocolContext,
final BlockHeader blockHeader,
final List<Transaction> transactions,
final Address miningBeneficiary,
final BlockHashLookup blockHashLookup,
final Wei blobGasPrice,
final Optional<BlockAccessListBuilder> blockAccessListBuilder,
final Optional<BlockAccessList> maybeBlockBal,
final Optional<BlockHeader> maybeParentHeader) {
if (!(protocolContext.getWorldStateArchive() instanceof PathBasedWorldStateProvider)) {
// Block-STM multi-version view requires a Bonsai/path-based world state.
return Optional.empty();
}
final AereBlockStmEngine engine = new AereBlockStmEngine(transactionProcessor, numThreads);
engine.runAsyncBlock(
protocolContext,
blockHeader,
transactions,
miningBeneficiary,
blockHashLookup,
blobGasPrice,
Runnable::run,
blockAccessListBuilder,
maybeParentHeader);
final BlockStmStats stats = engine.stats();
LOG.debug(
"AERE Block-STM block #{}: txns={} threads={} exec={} valid={} aborts={} committedParallel={}"
+ " verified={}",
blockHeader.getNumber(),
stats.numTxns(),
stats.numThreads(),
stats.totalExecutions(),
stats.totalValidations(),
stats.totalAborts(),
stats.committedParallel(),
stats.verified());
// Parseable single-line marker for the benchmark harness: the isolated EXECUTION-PHASE nanos
// (worker pool only, excludes the flat-snapshot mint AND the downstream serial Bonsai trie
// commit) plus the snapshot-mint nanos, per block. Gated on AERE_BLOCKSTM_STATS=1 so it is
// silent in normal operation and only emitted during benchmarking.
if ("1".equals(System.getenv("AERE_BLOCKSTM_STATS")) && stats.numTxns() > 0) {
LOG.info(
"AERE-BLOCKSTM-STATS blk={} n={} thr={} execNs={} snapNs={} exec={} aborts={}"
+ " committedParallel={} verified={}",
blockHeader.getNumber(),
stats.numTxns(),
stats.numThreads(),
stats.execPhaseNanos(),
stats.snapshotNanos(),
stats.totalExecutions(),
stats.totalAborts(),
stats.committedParallel(),
stats.verified());
}
return Optional.of(new PreprocessingContext(engine));
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.BlockProcessingResult;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.MutableWorldState;
import org.hyperledger.besu.ethereum.mainnet.AbstractBlockProcessor.PreprocessingFunction.NoPreprocessing;
import org.hyperledger.besu.ethereum.mainnet.AbstractBlockProcessor.TransactionReceiptFactory;
import org.hyperledger.besu.ethereum.mainnet.BalConfiguration;
import org.hyperledger.besu.ethereum.mainnet.BlockProcessor;
import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor;
import org.hyperledger.besu.ethereum.mainnet.MiningBeneficiaryCalculator;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpecBuilder;
import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList;
import org.hyperledger.besu.ethereum.mainnet.parallelization.MainnetParallelBlockProcessor;
import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldState;
import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldStateUpdateAccumulator;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* L1 block processor that executes a block's transactions with AERE Block-STM optimistic parallel
* execution, committing in block order to produce a state root bit-identical to sequential
* execution. It reuses Besu's parallel consume path ({@link MainnetParallelBlockProcessor}: the
* overridden {@code getTransactionProcessingResult} pulls per-transaction results from the
* preprocessing context) but swaps Besu's single-round parallelizer for the AERE multi-version
* Block-STM engine via {@link AereBlockStmPreprocessing}.
*
* <p>Fork gate: this processor is selected only for the AERE {@code AerePQC} (FUTURE_EIPS) hard fork
* and only when {@code AERE_BLOCKSTM=1}, so a single activation delivers PQC precompiles + EIP-2935
* + parallel execution together. On any parallel-phase failure the whole block falls back to
* sequential execution, so the processor can never persist an unverified state root.
*/
@SuppressWarnings("MissingSummary")
public class AereBlockStmProcessor extends MainnetParallelBlockProcessor {
private static final Logger LOG = LoggerFactory.getLogger(AereBlockStmProcessor.class);
private static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* @param transactionProcessor the EVM transaction processor
* @param transactionReceiptFactory receipt factory
* @param blockReward block reward
* @param miningBeneficiaryCalculator beneficiary calculator
* @param skipZeroBlockRewards whether to skip zero rewards
* @param protocolSchedule the protocol schedule
* @param balConfiguration BAL configuration
* @param metricsSystem metrics
*/
public AereBlockStmProcessor(
final MainnetTransactionProcessor transactionProcessor,
final TransactionReceiptFactory transactionReceiptFactory,
final Wei blockReward,
final MiningBeneficiaryCalculator miningBeneficiaryCalculator,
final boolean skipZeroBlockRewards,
final ProtocolSchedule protocolSchedule,
final BalConfiguration balConfiguration,
final MetricsSystem metricsSystem) {
super(
transactionProcessor,
transactionReceiptFactory,
blockReward,
miningBeneficiaryCalculator,
skipZeroBlockRewards,
protocolSchedule,
balConfiguration,
metricsSystem);
}
@Override
public BlockProcessingResult processBlock(
final ProtocolContext protocolContext,
final Blockchain blockchain,
final MutableWorldState worldState,
final Block block) {
return processBlock(protocolContext, blockchain, worldState, block, Optional.empty());
}
@Override
public BlockProcessingResult processBlock(
final ProtocolContext protocolContext,
final Blockchain blockchain,
final MutableWorldState worldState,
final Block block,
final Optional<BlockAccessList> blockAccessList) {
final BlockProcessingResult result =
super.processBlock(
protocolContext,
blockchain,
worldState,
block,
blockAccessList,
new AereBlockStmPreprocessing(transactionProcessor, NCPU));
if (result.isFailed()) {
LOG.info(
"AERE Block-STM processing failure for block #{} ({}); falling back to sequential",
block.getHeader().getNumber(),
block.getHash());
if (worldState instanceof BonsaiWorldState) {
((BonsaiWorldStateUpdateAccumulator) worldState.updater()).reset();
}
return super.processBlock(
protocolContext, blockchain, worldState, block, blockAccessList, new NoPreprocessing());
}
return result;
}
/** Builds {@link AereBlockStmProcessor}, mirroring Besu's parallel builder. */
public static class AereBlockStmProcessorBuilder
implements ProtocolSpecBuilder.BlockProcessorBuilder {
private final MetricsSystem metricsSystem;
/**
* @param metricsSystem metrics
*/
public AereBlockStmProcessorBuilder(final MetricsSystem metricsSystem) {
this.metricsSystem = metricsSystem;
}
@Override
public BlockProcessor apply(
final MainnetTransactionProcessor transactionProcessor,
final TransactionReceiptFactory transactionReceiptFactory,
final Wei blockReward,
final MiningBeneficiaryCalculator miningBeneficiaryCalculator,
final boolean skipZeroBlockRewards,
final ProtocolSchedule protocolSchedule,
final BalConfiguration balConfiguration) {
return new AereBlockStmProcessor(
transactionProcessor,
transactionReceiptFactory,
blockReward,
miningBeneficiaryCalculator,
skipZeroBlockRewards,
protocolSchedule,
balConfiguration,
metricsSystem);
}
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Wei;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
/**
* Read-only access to the immutable base (parent-block) world state. A {@code Base} read in
* MVMemory falls through to here. Implementations must be safe for concurrent reads (the base state
* does not change during block execution).
*/
@SuppressWarnings("MissingSummary")
public interface BaseReader {
/**
* Whether the account exists in base state.
*
* @param address the account
* @return true if present
*/
boolean accountExists(Address address);
/**
* Base nonce (0 if absent).
*
* @param address the account
* @return nonce
*/
long getNonce(Address address);
/**
* Base balance (ZERO if absent).
*
* @param address the account
* @return balance
*/
Wei getBalance(Address address);
/**
* Base code (EMPTY if absent).
*
* @param address the account
* @return code
*/
Bytes getCode(Address address);
/**
* Base storage slot value (ZERO if absent).
*
* @param address the account
* @param slot the slot
* @return value
*/
UInt256 getStorageValue(Address address, UInt256 slot);
}

View File

@ -0,0 +1,38 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
/**
* Aggregate statistics for one Block-STM run.
*
* @param numTxns transactions in the block
* @param numThreads worker-pool size
* @param totalExecutions total incarnations executed (excess over numTxns = re-executions)
* @param totalValidations total validation tasks run
* @param totalAborts validation failures that triggered re-execution
* @param committedParallel transactions committed from a parallel outcome (not sequential fallback)
* @param verified whether the run reached scheduler.done() with no hard failure
* @param snapshotNanos wall-clock nanos spent building the flat base pre-snapshot (single Bonsai
* view mint + wrapper), excluded from the parallel execution phase
* @param execPhaseNanos wall-clock nanos of the PARALLEL EXECUTION PHASE only (worker pool start to
* termination); excludes both the snapshot build and the downstream serial Bonsai trie commit
*/
@SuppressWarnings("MissingSummary")
public record BlockStmStats(
int numTxns,
int numThreads,
int totalExecutions,
int totalValidations,
int totalAborts,
int committedParallel,
boolean verified,
long snapshotNanos,
long execPhaseNanos) {}

View File

@ -0,0 +1,165 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.worldview.BonsaiWorldState;
import org.hyperledger.besu.evm.account.Account;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
/**
* A FLAT, immutable, in-memory pre-snapshot of the parent-block base world state, shared lock-free
* by every Block-STM worker.
*
* <p>Iteration-1 gave each worker its OWN frozen {@link BonsaiWorldState} view and routed every base
* read through a layered-trie traversal. Two costs dominated cheap-tx blocks and capped scaling on
* execution-bound blocks (task bottleneck #2): (1) minting {@code numThreads} BonsaiWorldState
* snapshots per block, and (2) re-walking the parent trie on every read, on every worker, on every
* re-execution incarnation, even for keys already seen.
*
* <p>This snapshot mints the parent Bonsai view exactly ONCE. Each distinct base value (account
* facet triple, or one storage slot) is materialised the first time any worker touches it and cached
* in a {@link ConcurrentHashMap}; every subsequent read by any worker/incarnation is a lock-free
* O(1) hashmap lookup with no trie traversal. Cold loads (cache misses) route through the single
* shared BonsaiWorldState and are serialised on {@link #loadLock} because a BonsaiWorldState is not
* safe for concurrent {@code get}; this cold path is one-time-per-key and negligible for
* execution-bound workloads, and the hot path is fully lock-free.
*
* <p>Serial-equivalence is preserved: the base state is immutable for the whole parallel round, so
* every worker observes identical parent values regardless of load order (parallelRoot ==
* sequentialRoot). An account is materialised as a whole {@link Shell} (existence + nonce + balance
* + code) in one trie hit, so the common pattern of reading several facets of the same account costs
* a single traversal.
*/
@SuppressWarnings("MissingSummary")
final class FlatBaseSnapshot implements BaseReader {
/** Immutable materialised account facet triple. */
private static final class Shell {
final boolean exists;
final long nonce;
final Wei balance;
final Bytes code;
Shell(final boolean exists, final long nonce, final Wei balance, final Bytes code) {
this.exists = exists;
this.nonce = nonce;
this.balance = balance;
this.code = code;
}
}
private static final Shell ABSENT = new Shell(false, 0L, Wei.ZERO, Bytes.EMPTY);
/** Composite key for a materialised storage slot. */
private static final class SlotKey {
private final Address address;
private final UInt256 slot;
private final int hash;
SlotKey(final Address address, final UInt256 slot) {
this.address = address;
this.slot = slot;
this.hash = Objects.hash(address, slot);
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SlotKey)) {
return false;
}
final SlotKey k = (SlotKey) o;
return address.equals(k.address) && slot.equals(k.slot);
}
@Override
public int hashCode() {
return hash;
}
}
private final BonsaiWorldState bws;
private final Object loadLock = new Object();
private final ConcurrentHashMap<Address, Shell> accounts = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SlotKey, UInt256> storage = new ConcurrentHashMap<>();
FlatBaseSnapshot(final BonsaiWorldState bws) {
this.bws = bws;
}
private Shell shell(final Address a) {
final Shell hit = accounts.get(a); // lock-free hot path
if (hit != null) {
return hit;
}
synchronized (loadLock) {
final Shell again = accounts.get(a);
if (again != null) {
return again;
}
final Account acct = bws.get(a);
final Shell shell =
acct == null
? ABSENT
: new Shell(true, acct.getNonce(), acct.getBalance(), acct.getCode());
accounts.put(a, shell);
return shell;
}
}
@Override
public boolean accountExists(final Address address) {
return shell(address).exists;
}
@Override
public long getNonce(final Address address) {
return shell(address).nonce;
}
@Override
public Wei getBalance(final Address address) {
return shell(address).balance;
}
@Override
public Bytes getCode(final Address address) {
return shell(address).code;
}
@Override
public UInt256 getStorageValue(final Address address, final UInt256 slot) {
final SlotKey key = new SlotKey(address, slot);
final UInt256 hit = storage.get(key); // lock-free hot path
if (hit != null) {
return hit;
}
synchronized (loadLock) {
final UInt256 again = storage.get(key);
if (again != null) {
return again;
}
final Account acct = bws.get(address);
final UInt256 value = acct == null ? UInt256.ZERO : acct.getStorageValue(slot);
storage.put(key, value);
return value;
}
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.evm.fluent.SimpleAccount;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
/**
* An account facade over multi-version memory for a single Block-STM incarnation. Reads of a storage
* slot are routed through MVMemory (highest lower-index write, else base), recording the observed
* provenance into the execution's read set. Writes buffer in the inherited {@link SimpleAccount}
* overlay and are extracted as the write set after execution.
*/
@SuppressWarnings("MissingSummary")
public class MvAccount extends SimpleAccount {
private final MvContext ctx;
private final Address addr;
// Values this incarnation started from (for write-set diffing).
private final long seededNonce;
private final Wei seededBalance;
private final Hash seededCodeHash;
MvAccount(
final MvContext ctx,
final Address address,
final long nonce,
final Wei balance,
final Bytes code) {
super(null, address, nonce, balance, code);
this.ctx = ctx;
this.addr = address;
this.seededNonce = nonce;
this.seededBalance = balance;
this.seededCodeHash = code == null || code.isEmpty() ? Hash.EMPTY : Hash.hash(code);
}
@Override
public UInt256 getOriginalStorageValue(final UInt256 key) {
final MvKey k = MvKey.storage(addr, key);
final MvMemory.ReadResult rr = ctx.mv.read(k, ctx.txnIdx);
if (rr.isBlocked()) {
if (ctx.blockedOn < 0) {
ctx.blockedOn = rr.blockingTxn();
}
return UInt256.ZERO;
}
if (!rr.descriptor().isBase()) {
ctx.record(k, rr.descriptor());
final Object v = rr.value();
return v == null ? UInt256.ZERO : (UInt256) v;
}
ctx.record(k, MvMemory.ReadDescriptor.BASE);
return ctx.base.getStorageValue(addr, key);
}
long seededNonce() {
return seededNonce;
}
Wei seededBalance() {
return seededBalance;
}
Hash seededCodeHash() {
return seededCodeHash;
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.evm.account.Account;
import org.hyperledger.besu.evm.account.MutableAccount;
import org.hyperledger.besu.evm.fluent.SimpleWorld;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.tuweni.bytes.Bytes;
/**
* The base (bottom) world layer for one Block-STM incarnation. It never holds writes: every EVM
* write lands in a child {@link SimpleWorld} layer obtained via {@link #updater()}. On a miss, this
* layer materialises an {@link MvAccount} by routing balance/nonce/code reads through MVMemory
* (highest lower-index write, else the immutable base state), recording provenance into the read
* set. Storage reads route lazily inside {@link MvAccount}.
*/
@SuppressWarnings("MissingSummary")
public class MvBaseLayer extends SimpleWorld {
private final MvContext ctx;
private final Map<Address, MvAccount> materialized = new HashMap<>();
private final Set<Address> absent = new HashSet<>();
MvBaseLayer(final MvContext ctx) {
super();
this.ctx = ctx;
}
@Override
public Account get(final Address address) {
return getAccount(address);
}
@Override
public MutableAccount getAccount(final Address address) {
final MvAccount m = materialized.get(address);
if (m != null) {
return m;
}
if (absent.contains(address)) {
return null;
}
final MvAccount created = materialize(address);
if (created == null) {
absent.add(address);
return null;
}
materialized.put(address, created);
return created;
}
/**
* The seeded (pre-execution) account, for write-set diffing.
*
* @param address the account
* @return the materialised account, or null if it was absent / freshly created in a child layer
*/
MvAccount materializedAccount(final Address address) {
return materialized.get(address);
}
private MvAccount materialize(final Address a) {
boolean exists = ctx.base.accountExists(a);
// balance (beneficiary balance is excluded from conflict tracking)
final Wei balance;
if (ctx.isBeneficiary(a)) {
balance = ctx.base.getBalance(a);
} else {
final MvKey k = MvKey.balance(a);
final MvMemory.ReadResult rr = ctx.mv.read(k, ctx.txnIdx);
if (rr.isBlocked()) {
block(rr);
balance = Wei.ZERO;
} else if (!rr.descriptor().isBase()) {
ctx.record(k, rr.descriptor());
balance = (Wei) rr.value();
exists = true;
} else {
ctx.record(k, MvMemory.ReadDescriptor.BASE);
balance = ctx.base.getBalance(a);
}
}
// nonce
final long nonce;
{
final MvKey k = MvKey.nonce(a);
final MvMemory.ReadResult rr = ctx.mv.read(k, ctx.txnIdx);
if (rr.isBlocked()) {
block(rr);
nonce = 0L;
} else if (!rr.descriptor().isBase()) {
ctx.record(k, rr.descriptor());
nonce = (Long) rr.value();
exists = true;
} else {
ctx.record(k, MvMemory.ReadDescriptor.BASE);
nonce = ctx.base.getNonce(a);
}
}
// code
final Bytes code;
{
final MvKey k = MvKey.code(a);
final MvMemory.ReadResult rr = ctx.mv.read(k, ctx.txnIdx);
if (rr.isBlocked()) {
block(rr);
code = Bytes.EMPTY;
} else if (!rr.descriptor().isBase()) {
ctx.record(k, rr.descriptor());
code = (Bytes) rr.value();
exists = true;
} else {
ctx.record(k, MvMemory.ReadDescriptor.BASE);
code = ctx.base.getCode(a);
}
}
if (!exists) {
// Absent account: reads recorded so a later creating write is caught by validation.
return null;
}
return new MvAccount(ctx, a, nonce, balance, code);
}
private void block(final MvMemory.ReadResult rr) {
if (ctx.blockedOn < 0) {
ctx.blockedOn = rr.blockingTxn();
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import org.hyperledger.besu.datatypes.Address;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Per-execution capture state for one incarnation of one transaction. Confined to the single worker
* thread executing that incarnation, so it needs no synchronization internally.
*
* <p>Records the read set (key -> observed provenance) for later validation, and the address of the
* mining beneficiary, whose balance is excluded from conflict detection (its reward is applied
* additively during in-order commit, mirroring Besu's {@code traceBeforeRewardTransaction} trick).
*/
@SuppressWarnings("MissingSummary")
public final class MvContext {
final int txnIdx;
final MvMemory mv;
final BaseReader base;
final Address beneficiary;
/** key -> first observed provenance (dedup on key, preserve encounter order). */
final Map<MvKey, MvMemory.ReadDescriptor> reads = new LinkedHashMap<>();
/** set to the blocking txn index (>=0) the first time a read hit an ESTIMATE. */
int blockedOn = -1;
MvContext(
final int txnIdx, final MvMemory mv, final BaseReader base, final Address beneficiary) {
this.txnIdx = txnIdx;
this.mv = mv;
this.base = base;
this.beneficiary = beneficiary;
}
void record(final MvKey key, final MvMemory.ReadDescriptor desc) {
reads.putIfAbsent(key, desc);
}
boolean isBeneficiary(final Address a) {
return beneficiary != null && beneficiary.equals(a);
}
}

View File

@ -0,0 +1,156 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import org.hyperledger.besu.datatypes.Address;
import java.util.Objects;
import org.apache.tuweni.units.bigints.UInt256;
/**
* A location in world state that Block-STM tracks for read/write-set conflict detection. Mirrors the
* Rust {@code Key} enum (Balance / Storage) generalised to the four EVM account facets that a
* transaction can read or write: BALANCE, NONCE, CODE and a STORAGE slot.
*
* <p>This is the unit over which MVMemory shards and over which read-set validation compares
* provenance. Two {@code MvKey}s are equal iff they refer to the same facet of the same account (and
* same slot for STORAGE).
*/
@SuppressWarnings("MissingSummary")
public final class MvKey {
/** The account facet this key addresses. */
public enum Kind {
/** Account balance. */
BALANCE,
/** Account nonce. */
NONCE,
/** Account code. */
CODE,
/** A single storage slot. */
STORAGE
}
private final Kind kind;
private final Address address;
private final UInt256 slot; // null unless STORAGE
private final int hash;
private MvKey(final Kind kind, final Address address, final UInt256 slot) {
this.kind = kind;
this.address = address;
this.slot = slot;
this.hash = Objects.hash(kind, address, slot);
}
/**
* Balance key.
*
* @param address the account
* @return the key
*/
public static MvKey balance(final Address address) {
return new MvKey(Kind.BALANCE, address, null);
}
/**
* Nonce key.
*
* @param address the account
* @return the key
*/
public static MvKey nonce(final Address address) {
return new MvKey(Kind.NONCE, address, null);
}
/**
* Code key.
*
* @param address the account
* @return the key
*/
public static MvKey code(final Address address) {
return new MvKey(Kind.CODE, address, null);
}
/**
* Storage-slot key.
*
* @param address the account
* @param slot the slot
* @return the key
*/
public static MvKey storage(final Address address, final UInt256 slot) {
return new MvKey(Kind.STORAGE, address, slot);
}
/**
* The facet kind.
*
* @return kind
*/
public Kind kind() {
return kind;
}
/**
* The account address.
*
* @return address
*/
public Address address() {
return address;
}
/**
* Deterministic shard index, mirroring the Rust FNV-1a spread.
*
* @param shards number of shards
* @return shard index in [0, shards)
*/
public int shard(final int shards) {
long h = 0xcbf29ce484222325L;
h = fnv(h, kind.ordinal() + 1L);
h = fnv(h, address.hashCode() & 0xffffffffL);
h = fnv(h, slot == null ? 0L : slot.hashCode() & 0xffffffffL);
int s = (int) Long.remainderUnsigned(h, shards);
return s;
}
private static long fnv(final long acc, final long b) {
long h = acc ^ b;
h *= 0x100000001b3L;
return h;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MvKey)) {
return false;
}
final MvKey k = (MvKey) o;
return kind == k.kind && address.equals(k.address) && Objects.equals(slot, k.slot);
}
@Override
public int hashCode() {
return hash;
}
@Override
public String toString() {
return kind + "(" + address + (slot == null ? "" : "," + slot.toShortHexString()) + ")";
}
}

View File

@ -0,0 +1,266 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* Multi-version shared memory (MVMemory). Faithful Java port of the AERE Rust {@code mvmemory.rs}
* (Gelashvili et al. 2022).
*
* <p>For every {@link MvKey}, MVMemory keeps an ordered map {@code txnIndex -> Cell}, where a cell
* is either a concrete value written by that transaction's latest incarnation, or an ESTIMATE
* placeholder (a marker that a lower transaction previously wrote here but is currently being
* re-executed, so its value is not trustworthy).
*
* <p>A read by transaction {@code i} for key {@code k} returns the entry of the HIGHEST transaction
* {@code j < i} that wrote {@code k}. That is what makes speculative parallel execution
* serial-equivalent: transaction {@code i} always observes the writes of all lower-indexed
* transactions that have run, and nothing from higher ones.
*
* <p>Storage is sharded across many independent locks keyed by a hash of the key, so
* non-conflicting transactions touch disjoint locks and run without contention.
*/
@SuppressWarnings("MissingSummary")
public class MvMemory {
private static final int SHARDS = 256;
/** What a read observed, recorded into the reader's read set for later validation. */
public enum DescKind {
/** Nothing below the reader wrote this key: it came from base state. */
BASE,
/** It was written by some (txnIndex, incarnation). */
VERSION
}
/** A read-set descriptor: BASE, or VERSION(txnIndex, incarnation). */
public static final class ReadDescriptor {
/** Shared BASE singleton. */
public static final ReadDescriptor BASE = new ReadDescriptor(DescKind.BASE, -1, -1);
private final DescKind kind;
private final int txnIndex;
private final int incarnation;
private ReadDescriptor(final DescKind kind, final int txnIndex, final int incarnation) {
this.kind = kind;
this.txnIndex = txnIndex;
this.incarnation = incarnation;
}
static ReadDescriptor version(final int txnIndex, final int incarnation) {
return new ReadDescriptor(DescKind.VERSION, txnIndex, incarnation);
}
/** @return true if this descriptor is a BASE read. */
public boolean isBase() {
return kind == DescKind.BASE;
}
boolean sameProvenance(final ReadDescriptor other) {
if (kind != other.kind) {
return false;
}
if (kind == DescKind.BASE) {
return true;
}
return txnIndex == other.txnIndex && incarnation == other.incarnation;
}
}
/** Live read outcome: OK(descriptor,value) or BLOCKED(blockingTxn). */
public static final class ReadResult {
final boolean blocked;
final int blockingTxn;
final ReadDescriptor descriptor;
final Object value;
private ReadResult(
final boolean blocked,
final int blockingTxn,
final ReadDescriptor descriptor,
final Object value) {
this.blocked = blocked;
this.blockingTxn = blockingTxn;
this.descriptor = descriptor;
this.value = value;
}
static ReadResult ok(final ReadDescriptor d, final Object v) {
return new ReadResult(false, -1, d, v);
}
static ReadResult blocked(final int blockingTxn) {
return new ReadResult(true, blockingTxn, null, null);
}
/** @return true if this read hit an ESTIMATE and the reader must park. */
public boolean isBlocked() {
return blocked;
}
/** @return the blocking txn index (valid iff {@link #isBlocked()}). */
public int blockingTxn() {
return blockingTxn;
}
/** @return the observed provenance descriptor (valid iff not blocked). */
public ReadDescriptor descriptor() {
return descriptor;
}
/** @return the observed value, or null for a BASE read (caller falls back to base state). */
public Object value() {
return value;
}
}
private static final class Cell {
final boolean estimate;
final int incarnation;
final Object value;
private Cell(final boolean estimate, final int incarnation, final Object value) {
this.estimate = estimate;
this.incarnation = incarnation;
this.value = value;
}
static Cell value(final int inc, final Object v) {
return new Cell(false, inc, v);
}
static final Cell ESTIMATE = new Cell(true, -1, null);
}
private final ReentrantLock[] locks = new ReentrantLock[SHARDS];
// shard -> (key -> (txnIndex -> Cell))
private final Map<MvKey, TreeMap<Integer, Cell>>[] shards;
/** New empty MVMemory. */
@SuppressWarnings({"unchecked", "rawtypes"})
public MvMemory() {
shards = new Map[SHARDS];
for (int i = 0; i < SHARDS; i++) {
locks[i] = new ReentrantLock();
shards[i] = new HashMap<>();
}
}
/**
* Read the value visible to {@code txnIdx}: the highest entry strictly below it.
*
* @param key the key
* @param txnIdx the reading transaction index
* @return the read result
*/
public ReadResult read(final MvKey key, final int txnIdx) {
final int s = key.shard(SHARDS);
locks[s].lock();
try {
final TreeMap<Integer, Cell> versions = shards[s].get(key);
if (versions != null) {
final Map.Entry<Integer, Cell> e = versions.lowerEntry(txnIdx);
if (e != null) {
final Cell cell = e.getValue();
if (cell.estimate) {
return ReadResult.blocked(e.getKey());
}
return ReadResult.ok(ReadDescriptor.version(e.getKey(), cell.incarnation), cell.value);
}
}
return ReadResult.ok(ReadDescriptor.BASE, null);
} finally {
locks[s].unlock();
}
}
/**
* Install {@code txnIdx}'s new write set (incarnation {@code inc}); remove stale entries the
* previous incarnation wrote but this one no longer does. Returns true if the written key-SET
* changed (a key added or removed) — Block-STM's "wrote a new path", which triggers revalidation
* of the suffix.
*
* @param txnIdx the writing transaction
* @param inc the incarnation
* @param writes the new write set
* @param prevKeys the keys written by the previous incarnation
* @return whether the written key-set changed
*/
public boolean applyWriteSet(
final int txnIdx,
final int inc,
final Map<MvKey, Object> writes,
final Set<MvKey> prevKeys) {
for (final Map.Entry<MvKey, Object> w : writes.entrySet()) {
final MvKey k = w.getKey();
final int s = k.shard(SHARDS);
locks[s].lock();
try {
shards[s].computeIfAbsent(k, kk -> new TreeMap<>()).put(txnIdx, Cell.value(inc, w.getValue()));
} finally {
locks[s].unlock();
}
}
boolean removedAny = false;
for (final MvKey k : prevKeys) {
if (!writes.containsKey(k)) {
final int s = k.shard(SHARDS);
locks[s].lock();
try {
final TreeMap<Integer, Cell> versions = shards[s].get(k);
if (versions != null) {
versions.remove(txnIdx);
}
} finally {
locks[s].unlock();
}
removedAny = true;
}
}
boolean addedAny = false;
for (final MvKey k : writes.keySet()) {
if (!prevKeys.contains(k)) {
addedAny = true;
break;
}
}
return addedAny || removedAny;
}
/**
* Convert all of {@code txnIdx}'s current writes (over {@code keys}) into ESTIMATE placeholders,
* so any concurrent higher transaction that reads one of these keys blocks on {@code txnIdx}
* instead of consuming a soon-to-be-wrong value.
*
* @param txnIdx the aborted transaction
* @param keys its written keys
*/
public void markEstimates(final int txnIdx, final Set<MvKey> keys) {
for (final MvKey k : keys) {
final int s = k.shard(SHARDS);
locks[s].lock();
try {
final TreeMap<Integer, Cell> versions = shards[s].get(k);
if (versions != null && versions.containsKey(txnIdx)) {
versions.put(txnIdx, Cell.ESTIMATE);
}
} finally {
locks[s].unlock();
}
}
}
}

View File

@ -0,0 +1,390 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.blockstm;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* The Block-STM collaborative scheduler (Gelashvili et al. 2022, Algorithm 3). Faithful Java port of
* the AERE Rust {@code scheduler.rs}.
*
* <p>Hands out EXECUTION and VALIDATION tasks to a worker pool. Two cursors ({@code executionIdx},
* {@code validationIdx}) drive it; validation of a lower index always takes priority over execution
* of a higher one. When a transaction aborts or writes a new path, the relevant cursor is decreased
* so the affected range is revisited. The block is done when both cursors have passed the end AND no
* task is in flight AND no decrease happened during the check. Dependencies are handled without
* condvars via parking on the blocker's dependency list.
*/
@SuppressWarnings("MissingSummary")
public class Scheduler {
/** Task type handed to a worker. */
public enum TaskKind {
/** Run incarnation {@code inc} of transaction {@code idx}. */
EXECUTION,
/** Re-check {@code idx}'s read set. */
VALIDATION,
/** No task available right now; retry. */
NO_TASK,
/** Block fully executed & validated. */
DONE
}
/** A unit of work, or a control signal. */
public static final class Task {
/** Shared NO_TASK singleton. */
public static final Task NO_TASK = new Task(TaskKind.NO_TASK, -1, -1);
/** Shared DONE singleton. */
public static final Task DONE = new Task(TaskKind.DONE, -1, -1);
final TaskKind kind;
final int idx;
final int inc;
Task(final TaskKind kind, final int idx, final int inc) {
this.kind = kind;
this.idx = idx;
this.inc = inc;
}
/** @return the task kind */
public TaskKind kind() {
return kind;
}
/** @return the transaction index */
public int idx() {
return idx;
}
/** @return the incarnation */
public int inc() {
return inc;
}
}
private enum StatusKind {
READY_TO_EXECUTE,
EXECUTING,
SUSPENDED,
EXECUTED,
ABORTING
}
private static final class Status {
StatusKind kind;
int inc;
Status(final StatusKind kind, final int inc) {
this.kind = kind;
this.inc = inc;
}
}
private final int numTxns;
private final AtomicInteger executionIdx = new AtomicInteger(0);
private final AtomicInteger validationIdx = new AtomicInteger(0);
private final AtomicInteger decreaseCnt = new AtomicInteger(0);
private final AtomicInteger numActiveTasks = new AtomicInteger(0);
private final AtomicBoolean done = new AtomicBoolean(false);
private final Status[] status;
private final ReentrantLock[] statusLocks;
private final List<Integer>[] dependencies;
private final ReentrantLock[] depLocks;
/**
* New scheduler over {@code numTxns} transactions.
*
* @param numTxns transaction count
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public Scheduler(final int numTxns) {
this.numTxns = numTxns;
this.status = new Status[numTxns];
this.statusLocks = new ReentrantLock[numTxns];
this.dependencies = new List[numTxns];
this.depLocks = new ReentrantLock[numTxns];
for (int i = 0; i < numTxns; i++) {
status[i] = new Status(StatusKind.READY_TO_EXECUTE, 0);
statusLocks[i] = new ReentrantLock();
dependencies[i] = new ArrayList<>();
depLocks[i] = new ReentrantLock();
}
}
/** @return true once the block is fully executed & validated. */
public boolean done() {
return done.get();
}
private void decreaseExecutionIdx(final int target) {
executionIdx.getAndUpdate(cur -> Math.min(cur, target));
decreaseCnt.incrementAndGet();
}
private void decreaseValidationIdx(final int target) {
validationIdx.getAndUpdate(cur -> Math.min(cur, target));
decreaseCnt.incrementAndGet();
}
private void checkDone() {
final int observed = decreaseCnt.get();
if (executionIdx.get() >= numTxns
&& validationIdx.get() >= numTxns
&& numActiveTasks.get() == 0
&& decreaseCnt.get() == observed) {
done.set(true);
}
}
private Task tryIncarnate(final int idx) {
if (idx < numTxns) {
statusLocks[idx].lock();
try {
if (status[idx].kind == StatusKind.READY_TO_EXECUTE) {
final int inc = status[idx].inc;
status[idx].kind = StatusKind.EXECUTING;
return new Task(TaskKind.EXECUTION, idx, inc);
}
} finally {
statusLocks[idx].unlock();
}
}
numActiveTasks.decrementAndGet();
return null;
}
private Task nextVersionToExecute() {
if (executionIdx.get() >= numTxns) {
checkDone();
return null;
}
numActiveTasks.incrementAndGet();
final int idx = executionIdx.getAndIncrement();
return tryIncarnate(idx);
}
private Task nextVersionToValidate() {
if (validationIdx.get() >= numTxns) {
checkDone();
return null;
}
numActiveTasks.incrementAndGet();
final int idx = validationIdx.getAndIncrement();
if (idx < numTxns) {
statusLocks[idx].lock();
try {
if (status[idx].kind == StatusKind.EXECUTED) {
return new Task(TaskKind.VALIDATION, idx, status[idx].inc);
}
} finally {
statusLocks[idx].unlock();
}
}
numActiveTasks.decrementAndGet();
return null;
}
/**
* Main entry point workers spin on.
*
* @return a task, NO_TASK (retry), or DONE
*/
public Task nextTask() {
int idle = 0;
while (true) {
if (done.get()) {
return Task.DONE;
}
final int v = validationIdx.get();
final int e = executionIdx.get();
Task task = (v < e) ? nextVersionToValidate() : nextVersionToExecute();
if (task == null && v < e) {
// The preferred lower-index validation had nothing runnable (its target is still
// EXECUTING on another worker). Rather than busy-draining validationIdx while pending
// independent EXECUTION tasks sit ungrabbed (which starved real parallel work and made
// the execution phase anti-scale with worker count), try to grab a pending execution.
// This is scheduling-order only: executing any READY incarnation in any order is already
// permitted by Block-STM, and read-set validation still re-checks and re-executes on any
// conflict, so the committed result stays bit-identical to sequential.
task = nextVersionToExecute();
}
if (task != null) {
return task;
}
if (done.get()) {
return Task.DONE;
}
// Nothing runnable right now: back off so idle workers stop ping-ponging the shared
// cursors/locks across cores (that cache-line contention slows the workers doing real
// execution). Pure CPU hint / brief park; does not alter task selection or ordering.
if (++idle < 128) {
Thread.onSpinWait();
} else {
java.util.concurrent.locks.LockSupport.parkNanos(Math.min((idle - 127) * 512L, 50_000L));
}
}
}
/**
* Register that {@code idx} depends on {@code blocking} (it read {@code blocking}'s ESTIMATE).
*
* @param idx the reader
* @param blocking the blocker
* @return true if {@code idx} was parked (caller drops the task); false if {@code blocking}
* already finished (caller re-executes {@code idx})
*/
public boolean addDependency(final int idx, final int blocking) {
statusLocks[blocking].lock();
try {
if (status[blocking].kind == StatusKind.EXECUTED) {
return false;
}
depLocks[blocking].lock();
try {
dependencies[blocking].add(idx);
} finally {
depLocks[blocking].unlock();
}
statusLocks[idx].lock();
try {
if (status[idx].kind == StatusKind.EXECUTING) {
status[idx].kind = StatusKind.SUSPENDED;
}
} finally {
statusLocks[idx].unlock();
}
} finally {
statusLocks[blocking].unlock();
}
numActiveTasks.decrementAndGet();
return true;
}
private void resume(final int idx) {
statusLocks[idx].lock();
try {
if (status[idx].kind == StatusKind.SUSPENDED) {
status[idx].kind = StatusKind.READY_TO_EXECUTE;
}
} finally {
statusLocks[idx].unlock();
}
}
private void setReadyAfterAbort(final int idx) {
statusLocks[idx].lock();
try {
if (status[idx].kind == StatusKind.ABORTING) {
status[idx].kind = StatusKind.READY_TO_EXECUTE;
status[idx].inc = status[idx].inc + 1;
}
} finally {
statusLocks[idx].unlock();
}
}
private void setExecuted(final int idx, final int inc) {
statusLocks[idx].lock();
try {
status[idx].kind = StatusKind.EXECUTED;
status[idx].inc = inc;
} finally {
statusLocks[idx].unlock();
}
}
/**
* Called after an execution task completes.
*
* @param idx transaction
* @param inc incarnation
* @param wroteNewPath whether the written key-set changed vs the previous incarnation
* @return a follow-up task (inheriting the active-task unit) or NO_TASK (releasing it)
*/
public Task finishExecution(final int idx, final int inc, final boolean wroteNewPath) {
setExecuted(idx, inc);
final List<Integer> deps;
depLocks[idx].lock();
try {
deps = new ArrayList<>(dependencies[idx]);
dependencies[idx].clear();
} finally {
depLocks[idx].unlock();
}
if (!deps.isEmpty()) {
final int minDep = Collections.min(deps);
for (final int d : deps) {
resume(d);
}
decreaseExecutionIdx(minDep);
}
if (validationIdx.get() > idx) {
if (wroteNewPath) {
decreaseValidationIdx(idx);
numActiveTasks.decrementAndGet();
return Task.NO_TASK;
} else {
return new Task(TaskKind.VALIDATION, idx, inc);
}
} else {
numActiveTasks.decrementAndGet();
return Task.NO_TASK;
}
}
/**
* Attempt to win the right to abort {@code idx} at {@code inc}.
*
* @param idx transaction
* @param inc incarnation
* @return true iff this caller flipped EXECUTED(inc) -> ABORTING(inc)
*/
public boolean tryValidationAbort(final int idx, final int inc) {
statusLocks[idx].lock();
try {
if (status[idx].kind == StatusKind.EXECUTED && status[idx].inc == inc) {
status[idx].kind = StatusKind.ABORTING;
return true;
}
return false;
} finally {
statusLocks[idx].unlock();
}
}
/**
* Called after a validation task.
*
* @param idx transaction
* @param aborted whether validation failed AND this caller won {@link #tryValidationAbort}
* @return a follow-up execution task (inheriting the active-task unit) or NO_TASK (releasing it)
*/
public Task finishValidation(final int idx, final boolean aborted) {
if (aborted) {
setReadyAfterAbort(idx);
decreaseValidationIdx(idx + 1);
if (executionIdx.get() > idx) {
final Task t = tryIncarnate(idx);
return t == null ? Task.NO_TASK : t;
}
}
numActiveTasks.decrementAndGet();
return Task.NO_TASK;
}
}

View File

@ -0,0 +1,252 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0004-aere-basefee-floor.patch.
*/
package org.hyperledger.besu.ethereum.mainnet.feemarket;
import org.hyperledger.besu.config.GenesisConfig;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.feemarket.TransactionPriceCalculator;
import java.util.Optional;
import org.apache.tuweni.units.bigints.UInt256s;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LondonFeeMarket implements BaseFeeMarket {
private static final Logger LOG = LoggerFactory.getLogger(LondonFeeMarket.class);
static final Wei DEFAULT_BASEFEE_INITIAL_VALUE = GenesisConfig.BASEFEE_AT_GENESIS_DEFAULT_VALUE;
static final long DEFAULT_BASEFEE_MAX_CHANGE_DENOMINATOR = 8L;
static final long DEFAULT_SLACK_COEFFICIENT = 2L;
private static final Wei DEFAULT_BASEFEE_FLOOR = Wei.of(7L);
// AERE base-fee-floor fork (fork-gated, no-op until an operator activates it).
//
// Raises the consensus EIP-1559 base-fee floor from the emergent 7-wei integer-division floor
// to a configurable value (default 1 Gwei) at and after a chosen fork block, so the public RPC
// can serve a TRUE, block-hash-verifiable baseFeePerGas instead of a proxy-faked constant.
//
// Activation is read once at fee-market construction (node start) from a JVM system property
// with an environment-variable fallback, mirroring how the AERE PQC/Block-STM fork reads its
// own JVM-level configuration:
// -Daere.basefee.floor.forkBlock=<n> (env AERE_BASEFEE_FLOOR_FORKBLOCK) default Long.MAX_VALUE (UNSET => no-op)
// -Daere.basefee.floor.value=<wei> (env AERE_BASEFEE_FLOOR_VALUE) default 1_000_000_000 (1 Gwei)
//
// UNSET (default) means the fork block is Long.MAX_VALUE, so no real block ever reaches it and
// every code path below is byte-identical to upstream London behaviour (7-wei emergent floor).
static final long DEFAULT_AERE_BASEFEE_FLOOR_VALUE = 1_000_000_000L; // 1 Gwei in wei
private static final String AERE_FLOOR_FORK_BLOCK_PROPERTY = "aere.basefee.floor.forkBlock";
private static final String AERE_FLOOR_FORK_BLOCK_ENV = "AERE_BASEFEE_FLOOR_FORKBLOCK";
private static final String AERE_FLOOR_VALUE_PROPERTY = "aere.basefee.floor.value";
private static final String AERE_FLOOR_VALUE_ENV = "AERE_BASEFEE_FLOOR_VALUE";
protected final Wei baseFeeInitialValue;
private final long londonForkBlockNumber;
private final TransactionPriceCalculator txPriceCalculator;
private final Wei baseFeeFloor;
// Activation block for the AERE base-fee floor. Long.MAX_VALUE == UNSET == no-op.
private final long aereBaseFeeFloorForkBlock;
// Floor value applied at and after aereBaseFeeFloorForkBlock (wei).
private final long aereBaseFeeFloorValue;
LondonFeeMarket(final long londonForkBlockNumber, final Optional<Wei> baseFeePerGasOverride) {
this(TransactionPriceCalculator.eip1559(), londonForkBlockNumber, baseFeePerGasOverride);
}
LondonFeeMarket(
final TransactionPriceCalculator txPriceCalculator,
final long londonForkBlockNumber,
final Optional<Wei> baseFeePerGasOverride) {
// Production path: resolve the AERE base-fee-floor fork activation from JVM configuration.
this(
txPriceCalculator,
londonForkBlockNumber,
baseFeePerGasOverride,
readAereFloorForkBlockConfig(),
readAereFloorValueConfig());
}
// Package-private constructor taking explicit AERE floor-fork parameters. Used by the
// production constructor above (after resolving JVM config) and by unit tests so the fork can be
// exercised deterministically without mutating global JVM system properties / environment.
LondonFeeMarket(
final TransactionPriceCalculator txPriceCalculator,
final long londonForkBlockNumber,
final Optional<Wei> baseFeePerGasOverride,
final long aereBaseFeeFloorForkBlock,
final long aereBaseFeeFloorValue) {
this.txPriceCalculator = txPriceCalculator;
this.londonForkBlockNumber = londonForkBlockNumber;
this.baseFeeInitialValue = baseFeePerGasOverride.orElse(DEFAULT_BASEFEE_INITIAL_VALUE);
this.baseFeeFloor = baseFeeInitialValue.isZero() ? Wei.ZERO : DEFAULT_BASEFEE_FLOOR;
this.aereBaseFeeFloorForkBlock = aereBaseFeeFloorForkBlock;
this.aereBaseFeeFloorValue = aereBaseFeeFloorValue;
}
private static long readAereFloorForkBlockConfig() {
return readLongConfig(
AERE_FLOOR_FORK_BLOCK_PROPERTY, AERE_FLOOR_FORK_BLOCK_ENV, Long.MAX_VALUE);
}
private static long readAereFloorValueConfig() {
return readLongConfig(
AERE_FLOOR_VALUE_PROPERTY, AERE_FLOOR_VALUE_ENV, DEFAULT_AERE_BASEFEE_FLOOR_VALUE);
}
private static long readLongConfig(
final String systemProperty, final String envVar, final long defaultValue) {
String raw = System.getProperty(systemProperty);
if (raw == null || raw.isBlank()) {
raw = System.getenv(envVar);
}
if (raw == null || raw.isBlank()) {
return defaultValue;
}
try {
final long parsed = Long.parseLong(raw.trim());
if (parsed < 0L) {
LOG.warn(
"Ignoring negative {}/{} value '{}', using default {}",
systemProperty,
envVar,
raw,
defaultValue);
return defaultValue;
}
return parsed;
} catch (final NumberFormatException e) {
LOG.warn(
"Ignoring unparseable {}/{} value '{}', using default {}",
systemProperty,
envVar,
raw,
defaultValue);
return defaultValue;
}
}
// Clamp a computed baseFee up to the active AERE floor at and after the fork block. Before the
// fork block (and when UNSET, since Long.MAX_VALUE is never reached) this returns baseFee
// unchanged, keeping pre-fork behaviour byte-identical to upstream.
private Wei applyAereBaseFeeFloor(final long blockNumber, final Wei baseFee) {
if (blockNumber >= aereBaseFeeFloorForkBlock) {
return UInt256s.max(baseFee, Wei.of(aereBaseFeeFloorValue));
}
return baseFee;
}
// Effective tx-pool minimum-fee floor. Pre-fork (UNSET) this is exactly the legacy baseFeeFloor
// (7 wei, or 0 on a zero-basefee chain), so tx-pool acceptance is byte-identical. Once an
// operator activates the AERE base-fee-floor fork, the tx-pool floor rises to the active AERE
// floor so tx-pool pricing agrees with the >= 1 Gwei header baseFee produced by computeBaseFee.
// A zero-basefee chain (baseFeeFloor == 0) is never raised.
private Wei effectiveBaseFeeFloor() {
if (baseFeeFloor.isZero() || aereBaseFeeFloorForkBlock == Long.MAX_VALUE) {
return baseFeeFloor;
}
return UInt256s.max(baseFeeFloor, Wei.of(aereBaseFeeFloorValue));
}
@Override
public long getBasefeeMaxChangeDenominator() {
return DEFAULT_BASEFEE_MAX_CHANGE_DENOMINATOR;
}
@Override
public Wei getInitialBasefee() {
return baseFeeInitialValue;
}
@Override
public long getSlackCoefficient() {
return DEFAULT_SLACK_COEFFICIENT;
}
@Override
public TransactionPriceCalculator getTransactionPriceCalculator() {
return txPriceCalculator;
}
@Override
public boolean satisfiesFloorTxFee(final Transaction txn) {
// ensure effective baseFee is at least above floor
return txn.getGasPrice()
.map(Optional::of)
.orElse(txn.getMaxFeePerGas())
.filter(fee -> fee.greaterOrEqualThan(effectiveBaseFeeFloor()))
.isPresent();
}
@Override
public Wei computeBaseFee(
final long blockNumber,
final Wei parentBaseFee,
final long parentBlockGasUsed,
final long targetGasUsed) {
if (londonForkBlockNumber == blockNumber) {
return applyAereBaseFeeFloor(blockNumber, getInitialBasefee());
}
long gasDelta;
Wei feeDelta, baseFee;
if (parentBlockGasUsed == targetGasUsed) {
return applyAereBaseFeeFloor(blockNumber, parentBaseFee);
} else if (parentBlockGasUsed > targetGasUsed) {
gasDelta = parentBlockGasUsed - targetGasUsed;
final long denominator = getBasefeeMaxChangeDenominator();
feeDelta =
UInt256s.max(
parentBaseFee.multiply(gasDelta).divide(targetGasUsed).divide(denominator), Wei.ONE);
baseFee = parentBaseFee.add(feeDelta);
} else {
gasDelta = targetGasUsed - parentBlockGasUsed;
final long denominator = getBasefeeMaxChangeDenominator();
feeDelta = parentBaseFee.multiply(gasDelta).divide(targetGasUsed).divide(denominator);
baseFee = parentBaseFee.subtract(feeDelta);
}
LOG.trace(
"block #{} parentBaseFee: {} parentGasUsed: {} parentGasTarget: {} baseFee: {}",
blockNumber,
parentBaseFee,
parentBlockGasUsed,
targetGasUsed,
baseFee);
return applyAereBaseFeeFloor(blockNumber, baseFee);
}
@Override
public ValidationMode baseFeeValidationMode(final long blockNumber) {
return londonForkBlockNumber == blockNumber ? ValidationMode.INITIAL : ValidationMode.ONGOING;
}
@Override
public ValidationMode gasLimitValidationMode(final long blockNumber) {
return londonForkBlockNumber == blockNumber ? ValidationMode.INITIAL : ValidationMode.ONGOING;
}
@Override
public boolean isBeforeForkBlock(final long blockNumber) {
return londonForkBlockNumber > blockNumber;
}
}

View File

@ -0,0 +1,241 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.ethereum.core;
import org.hyperledger.besu.crypto.CodeDelegationSignature;
import org.hyperledger.besu.crypto.KeyPair;
import org.hyperledger.besu.crypto.SignatureAlgorithm;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.AccessListEntry;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.VersionedHash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.encoding.CodeDelegationTransactionEncoder;
import org.hyperledger.besu.ethereum.core.kzg.BlobsWithCommitments;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import java.math.BigInteger;
import java.util.List;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
public class TransactionTestFixture {
private static final SignatureAlgorithm SIGNATURE_ALGORITHM =
SignatureAlgorithmFactory.getInstance();
private static final KeyPair KEY_PAIR = SIGNATURE_ALGORITHM.generateKeyPair();
private static final org.hyperledger.besu.datatypes.CodeDelegation CODE_DELEGATION =
createSignedCodeDelegation(BigInteger.ZERO, Address.ZERO, 0, KEY_PAIR);
private TransactionType transactionType = TransactionType.FRONTIER;
private long nonce = 0;
private Optional<Wei> gasPrice = Optional.empty();
private long gasLimit = 5000;
private Optional<Address> to = Optional.empty();
private Address sender = Address.fromHexString(String.format("%020x", 1));
private Wei value = Wei.of(4);
private Bytes payload = Bytes.EMPTY;
private Optional<BigInteger> chainId = Optional.of(BigInteger.valueOf(1337));
private Optional<Wei> maxPriorityFeePerGas = Optional.empty();
private Optional<Wei> maxFeePerGas = Optional.empty();
private Optional<Wei> maxFeePerBlobGas = Optional.empty();
private Optional<List<AccessListEntry>> accessListEntries = Optional.empty();
private Optional<List<VersionedHash>> versionedHashes = Optional.empty();
private Optional<BlobsWithCommitments> blobs = Optional.empty();
private Optional<BigInteger> v = Optional.empty();
private Optional<List<org.hyperledger.besu.datatypes.CodeDelegation>> codeDelegations =
Optional.empty();
public Transaction createTransaction(final KeyPair keys) {
final Transaction.Builder builder = Transaction.builder();
builder
.type(transactionType)
.gasLimit(gasLimit)
.nonce(nonce)
.payload(payload)
.value(value)
.sender(sender);
switch (transactionType) {
case FRONTIER:
builder.gasPrice(gasPrice.orElse(Wei.of(5000)));
break;
case ACCESS_LIST:
builder.gasPrice(gasPrice.orElse(Wei.of(5000)));
builder.accessList(accessListEntries.orElse(List.of()));
break;
case EIP1559:
builder.maxPriorityFeePerGas(maxPriorityFeePerGas.orElse(Wei.of(500)));
builder.maxFeePerGas(maxFeePerGas.orElse(Wei.of(5000)));
builder.accessList(accessListEntries.orElse(List.of()));
break;
case BLOB:
builder.maxPriorityFeePerGas(maxPriorityFeePerGas.orElse(Wei.of(500)));
builder.maxFeePerGas(maxFeePerGas.orElse(Wei.of(5000)));
builder.accessList(accessListEntries.orElse(List.of()));
builder.maxFeePerBlobGas(maxFeePerBlobGas.orElse(Wei.ONE));
if (blobs.isPresent()) {
builder.kzgBlobs(
blobs.get().getBlobType(),
blobs.get().getKzgCommitments(),
blobs.get().getBlobs(),
blobs.get().getKzgProofs());
} else versionedHashes.ifPresent(builder::versionedHashes);
break;
case DELEGATE_CODE:
builder.maxPriorityFeePerGas(maxPriorityFeePerGas.orElse(Wei.of(500)));
builder.maxFeePerGas(maxFeePerGas.orElse(Wei.of(5000)));
builder.accessList(accessListEntries.orElse(List.of()));
builder.codeDelegations(codeDelegations.orElse(List.of(CODE_DELEGATION)));
break;
case AERE_PQ:
throw new IllegalArgumentException(
"AIP-20 transactions need ML-DSA keys; this fixture does not create them");
}
to.ifPresent(builder::to);
chainId.ifPresent(builder::chainId);
v.ifPresent(builder::v);
return builder.signAndBuild(keys);
}
public TransactionTestFixture type(final TransactionType transactionType) {
this.transactionType = transactionType;
return this;
}
public TransactionTestFixture nonce(final long nonce) {
this.nonce = nonce;
return this;
}
public TransactionTestFixture gasPrice(final Wei gasPrice) {
this.gasPrice = Optional.ofNullable(gasPrice);
return this;
}
public TransactionTestFixture gasLimit(final long gasLimit) {
this.gasLimit = gasLimit;
return this;
}
public TransactionTestFixture to(final Optional<Address> to) {
this.to = to;
return this;
}
public TransactionTestFixture sender(final Address sender) {
this.sender = sender;
return this;
}
public TransactionTestFixture value(final Wei value) {
this.value = value;
return this;
}
public TransactionTestFixture payload(final Bytes payload) {
this.payload = payload;
return this;
}
public TransactionTestFixture chainId(final Optional<BigInteger> chainId) {
this.chainId = chainId;
return this;
}
public TransactionTestFixture maxPriorityFeePerGas(final Optional<Wei> maxPriorityFeePerGas) {
this.maxPriorityFeePerGas = maxPriorityFeePerGas;
return this;
}
public TransactionTestFixture maxFeePerGas(final Optional<Wei> maxFeePerGas) {
this.maxFeePerGas = maxFeePerGas;
return this;
}
public TransactionTestFixture maxFeePerBlobGas(final Optional<Wei> maxFeePerBlobGas) {
this.maxFeePerBlobGas = maxFeePerBlobGas;
return this;
}
public TransactionTestFixture accessList(final List<AccessListEntry> accessListEntries) {
this.accessListEntries = Optional.ofNullable(accessListEntries);
return this;
}
public TransactionTestFixture versionedHashes(
final Optional<List<VersionedHash>> versionedHashes) {
this.versionedHashes = versionedHashes;
return this;
}
public TransactionTestFixture v(final Optional<BigInteger> v) {
this.v = v;
return this;
}
public TransactionTestFixture blobsWithCommitments(final Optional<BlobsWithCommitments> blobs) {
this.blobs = blobs;
return this;
}
public TransactionTestFixture codeDelegations(
final List<org.hyperledger.besu.datatypes.CodeDelegation> codeDelegations) {
this.codeDelegations = Optional.ofNullable(codeDelegations);
return this;
}
public static CodeDelegation createSignedCodeDelegation(
final BigInteger chainId, final Address address, final long nonce, final KeyPair keys) {
BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput();
CodeDelegationTransactionEncoder.encodeSingleCodeDelegationWithoutSignature(
new org.hyperledger.besu.ethereum.core.CodeDelegation(chainId, address, nonce, null),
rlpOutput);
final Hash hash =
Hash.hash(
Bytes.concatenate(
org.hyperledger.besu.ethereum.core.CodeDelegation.MAGIC, rlpOutput.encoded()));
final var signature = SIGNATURE_ALGORITHM.sign(Bytes32.wrap(hash.getBytes()), keys);
return new org.hyperledger.besu.ethereum.core.CodeDelegation(
chainId,
address,
nonce,
CodeDelegationSignature.create(signature.getR(), signature.getS(), signature.getRecId()));
}
}

View File

@ -0,0 +1,210 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.core;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.hyperledger.besu.datatypes.AerePqAlgorithm;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.BlobType;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.GasLimitCalculator;
import org.hyperledger.besu.ethereum.core.encoding.EncodingContext;
import org.hyperledger.besu.ethereum.core.encoding.TransactionDecoder;
import org.hyperledger.besu.ethereum.core.encoding.TransactionEncoder;
import org.hyperledger.besu.ethereum.mainnet.AerePqTransactionSupport;
import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionValidator;
import org.hyperledger.besu.ethereum.mainnet.TransactionValidationParams;
import org.hyperledger.besu.ethereum.mainnet.ValidationResult;
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket;
import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason;
import org.hyperledger.besu.evm.gascalculator.OsakaGasCalculator;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.Test;
/**
* AIP-20 conformance on the Besu-derived client, against the vectors produced by the reference tooling
* (`tools/pqtx/pqtx.mjs vectors`, @noble/post-quantum): the same bytes must decode to the same sender and
* hash, verify under Bouncy Castle's ML-DSA, be charged the AIP-20 gas, and every negative vector must be
* refused for the reason the vector names. Two independent ML-DSA implementations agreeing on the same
* signature is the cross-client property the AIP requires.
*/
class AerePqTransactionTest {
private static final BigInteger CHAIN_ID = BigInteger.valueOf(28001);
private static String vector(final String id) throws IOException {
try (InputStream in =
AerePqTransactionTest.class.getResourceAsStream("/aere/pqtx/" + id + ".json")) {
assertThat(in).as("vector " + id + " on the test classpath").isNotNull();
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
private static String field(final String json, final String name) {
final Matcher m = Pattern.compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json);
assertThat(m.find()).as("field " + name).isTrue();
return m.group(1);
}
private static Transaction decode(final String rawHex) {
return TransactionDecoder.decodeOpaqueBytes(
Bytes.fromHexString(rawHex), EncodingContext.POOLED_TRANSACTION);
}
private static MainnetTransactionValidator validator() {
return new MainnetTransactionValidator(
new OsakaGasCalculator(),
GasLimitCalculator.constant(),
FeeMarket.london(0L),
true,
Optional.of(CHAIN_ID),
Set.of(TransactionType.EIP1559, TransactionType.AERE_PQ),
Set.of(BlobType.KZG_CELL_PROOFS),
Integer.MAX_VALUE);
}
@Test
void validVectorsDecodeToTheReferenceSenderAndHashAndRoundTrip() throws IOException {
for (final int alg : new int[] {1, 2, 3}) {
final String v = vector("valid-ml-dsa-" + alg);
final Transaction tx = decode(field(v, "raw"));
assertThat(tx.getType()).isEqualTo(TransactionType.AERE_PQ);
assertThat(tx.getPqAuthorization()).isPresent();
assertThat(tx.getPqAuthorization().get().algorithm())
.isEqualTo(AerePqAlgorithm.byId(alg).orElseThrow());
assertThat(tx.getSignature()).as("no secp256k1 signature on a type 0x50 tx").isNull();
assertThat(tx.getSender()).isEqualTo(Address.fromHexString(field(v, "sender")));
assertThat(tx.getHash()).isEqualTo(Hash.fromHexString(field(v, "hash")));
assertThat(tx.getPqSigningHash()).isEqualTo(Bytes.fromHexString(field(v, "signingHash")));
assertThat(tx.getChainId()).contains(CHAIN_ID);
assertThat(tx.getR()).isNull();
assertThat(tx.getYParity()).isNull();
final Bytes reEncoded = TransactionEncoder.encodeOpaqueBytes(tx, EncodingContext.BLOCK_BODY);
assertThat(reEncoded).as("round trip, alg " + alg).isEqualTo(Bytes.fromHexString(field(v, "raw")));
// a detached copy keeps everything that identifies the transaction
final Transaction copy = tx.detachedCopy();
assertThat(copy.getHash()).isEqualTo(tx.getHash());
assertThat(copy.getSender()).isEqualTo(tx.getSender());
assertThat(copy).isEqualTo(tx);
}
}
@Test
void validVectorsVerifyUnderBouncyCastleAndPassTheValidator() throws IOException {
for (final int alg : new int[] {1, 2, 3}) {
final Transaction tx = decode(field(vector("valid-ml-dsa-" + alg), "raw"));
final ValidationResult<TransactionInvalidReason> auth =
AerePqTransactionSupport.validateAuthorization(tx);
assertThat(auth.isValid())
.withFailMessage(() -> "ML-DSA verification, alg " + alg + ": " + auth.getErrorMessage())
.isTrue();
final ValidationResult<TransactionInvalidReason> full =
validator()
.validate(
tx, Optional.of(Wei.of(1_000_000_000L)), Optional.of(Wei.ZERO), TransactionValidationParams.processingBlock());
assertThat(full.isValid())
.withFailMessage(() -> "validator, alg " + alg + ": " + full.getErrorMessage())
.isTrue();
}
}
@Test
void aip20GasIsChargedForTheAuthorization() throws IOException {
final Transaction tx = decode(field(vector("valid-ml-dsa-2"), "raw"));
final long intrinsic = new OsakaGasCalculator().transactionIntrinsicGasCost(tx, 0L);
// 21,000 + 16 * (1,952 + 3,309) + 75,000 = 180,176 (AIP-20 worked example, empty calldata)
assertThat(intrinsic).isEqualTo(180_176L);
final Transaction tx87 = decode(field(vector("valid-ml-dsa-3"), "raw"));
assertThat(new OsakaGasCalculator().transactionIntrinsicGasCost(tx87, 0L))
.isEqualTo(21_000L + 16L * (2_592L + 4_627L) + 100_000L);
}
@Test
void aCorruptedSignatureIsInvalidSignature() throws IOException {
for (final int alg : new int[] {1, 2, 3}) {
final Transaction tx = decode(field(vector("bad-signature-ml-dsa-" + alg), "raw"));
final ValidationResult<TransactionInvalidReason> r =
AerePqTransactionSupport.validateAuthorization(tx);
assertThat(r.isValid()).isFalse();
assertThat(r.getInvalidReason()).isEqualTo(TransactionInvalidReason.INVALID_SIGNATURE);
}
}
@Test
void theWrongChainIdIsRefusedByTheValidator() throws IOException {
final Transaction tx = decode(field(vector("wrong-chain-ml-dsa-2"), "raw"));
final ValidationResult<TransactionInvalidReason> r =
validator()
.validate(tx, Optional.of(Wei.of(1_000_000_000L)), Optional.of(Wei.ZERO), TransactionValidationParams.processingBlock());
assertThat(r.isValid()).isFalse();
assertThat(r.getInvalidReason()).isEqualTo(TransactionInvalidReason.WRONG_CHAIN_ID);
}
@Test
void aShortSignatureAndAnUnknownAlgorithmAreRefusedBeforeAnyCryptography() throws IOException {
for (final int alg : new int[] {1, 2, 3}) {
final String raw = field(vector("short-signature-ml-dsa-" + alg), "raw");
assertThatThrownBy(() -> decode(raw))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("byte signature");
}
final String unknown = field(vector("unknown-alg-0x10"), "raw");
assertThatThrownBy(() -> decode(unknown))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("unknown post-quantum algorithm identifier 16");
}
@Test
void theTypeIsNeverActiveWithoutTheForkBlockProperty() {
// the test JVM does not set aere.pqtx.forkBlock, so the rule is "never": the same as a binary without it
assertThat(AerePqTransactionSupport.forkBlock()).isEqualTo(Long.MAX_VALUE);
assertThat(AerePqTransactionSupport.isActiveAt(Long.MAX_VALUE - 1)).isFalse();
assertThat(AerePqTransactionSupport.inactiveMessage(1)).contains("not enabled");
}
@Test
void aClassicalTransactionCarriesNoAuthorizationAndStillWorks() {
final Transaction tx =
Transaction.builder()
.type(TransactionType.EIP1559)
.chainId(CHAIN_ID)
.nonce(0)
.maxPriorityFeePerGas(Wei.ONE)
.maxFeePerGas(Wei.ONE)
.gasLimit(21_000)
.to(Address.ZERO)
.value(Wei.ZERO)
.payload(Bytes.EMPTY)
.signAndBuild(
org.hyperledger.besu.crypto.SignatureAlgorithmFactory.getInstance().generateKeyPair());
assertThat(tx.getPqAuthorization()).isEmpty();
assertThat(tx.getR()).isNotNull();
assertThat(new OsakaGasCalculator().transactionIntrinsicGasCost(tx, 0L)).isEqualTo(21_000L);
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright contributors to Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Aere Network notice (Apache-2.0 section 4(b)): this file was added by Aere Network, 2026-09-28 (D-432).
*/
package org.hyperledger.besu.ethereum.mainnet;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.crypto.KeyPair;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.BlobType;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.GasLimitCalculator;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket;
import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason;
import org.hyperledger.besu.evm.gascalculator.OsakaGasCalculator;
import java.math.BigInteger;
import java.util.Optional;
import java.util.Set;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.Test;
/**
* AERE D-432: the EIP-7825 transaction gas cap is compared UNSIGNED. The decoder reads gasLimit with readLongScalar, so a limit
* of 2^63 or more is a negative long here; a signed compare let it through the cap, while the second client refuses it at
* decoding. Found by the AIP-20 differential fuzzer (aips/aip20-conformitate/fuzz-pqtx).
*/
class AereD432GasCapUnsignedTest {
private static final long CAP = 1L << 24;
private static final BigInteger CHAIN = BigInteger.valueOf(28001);
private static final KeyPair KEYS = SignatureAlgorithmFactory.getInstance().generateKeyPair();
private static MainnetTransactionValidator validator() {
final GasLimitCalculator glc =
new GasLimitCalculator() {
@Override
public long nextGasLimit(final long currentGasLimit, final long targetGasLimit, final long newBlockNumber) {
return currentGasLimit;
}
@Override
public long transactionGasLimitCap() {
return CAP;
}
};
return new MainnetTransactionValidator(
new OsakaGasCalculator(), glc, FeeMarket.london(0L), true, Optional.of(CHAIN), Set.of(TransactionType.EIP1559),
Set.of(BlobType.KZG_CELL_PROOFS), 2 * 24576);
}
private static ValidationResult<TransactionInvalidReason> judge(final long gasLimit) {
final Transaction tx =
Transaction.builder()
.type(TransactionType.EIP1559)
.chainId(CHAIN)
.nonce(0)
.maxPriorityFeePerGas(Wei.ONE)
.maxFeePerGas(Wei.of(2_000_000_000L))
.gasLimit(gasLimit)
.to(Address.ZERO)
.value(Wei.ZERO)
.payload(Bytes.EMPTY)
.signAndBuild(KEYS);
return validator().validate(tx, Optional.of(Wei.of(1_000_000_000L)), Optional.empty(), TransactionValidationParams.processingBlock());
}
@Test
void aGasLimitOf2To63OrMoreIsRefusedByTheCapNotLetThroughAsANegativeLong() {
// 2^63, 2^63 + 5 and 2^64 - 1, as readLongScalar hands them over
for (final long g : new long[] {Long.MIN_VALUE, Long.MIN_VALUE + 5, -1L}) {
final ValidationResult<TransactionInvalidReason> r = judge(g);
assertThat(r.isValid()).withFailMessage("gasLimit %s admitted", Long.toUnsignedString(g)).isFalse();
assertThat(r.getInvalidReason())
.withFailMessage("gasLimit %s refused for %s, not the cap", Long.toUnsignedString(g), r.getInvalidReason())
.isEqualTo(TransactionInvalidReason.EXCEEDS_TRANSACTION_GAS_LIMIT);
}
}
@Test
void theCapItselfIsAdmittedAndOneAboveIsRefused() {
// positive control: the rule is armed at exactly 2^24 and the fixture is otherwise valid
assertThat(judge(CAP).isValid()).withFailMessage(() -> "gasLimit = cap refused: " + judge(CAP).getErrorMessage()).isTrue();
assertThat(judge(CAP + 1).getInvalidReason()).isEqualTo(TransactionInvalidReason.EXCEEDS_TRANSACTION_GAS_LIMIT);
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright contributors to Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Aere Network notice (Apache-2.0 section 4(b)): this file was added by Aere Network, 2026-09-28 (D-433), as the regression
* test of the ported upstream fixes hyperledger/besu ffe8104ee2 (GHSA-p4h2-gvh4-pv6j) and b7f1387cb4.
*/
package org.hyperledger.besu.ethereum.mainnet;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.GWei;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockDataGenerator;
import org.hyperledger.besu.ethereum.core.Withdrawal;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.apache.tuweni.units.bigints.UInt64;
import org.junit.jupiter.api.Test;
/**
* AERE D-433: on a BFT chain (QBFT on 2800 and 28001, Shanghai active from genesis) withdrawals are not applicable, and the
* BFT schedule installs NotApplicableWithdrawals while keeping Shanghai's WithdrawalsProcessor. Before the port the validator
* returned true for anything, so a single Byzantine proposer could put withdrawals in a block and every honest Besu node would
* credit them as native balance. The port (upstream ffe8104ee2) refuses a non-empty list and a withdrawals root other than the
* empty trie root.
*/
class AereD433BftWithdrawalsTest {
private static final WithdrawalsValidator V = new WithdrawalsValidator.NotApplicableWithdrawals();
@Test
void aByzantineWithdrawalListAndItsRootAreRefused() {
final List<Withdrawal> injected =
List.of(new Withdrawal(UInt64.valueOf(1), UInt64.valueOf(2), Address.fromHexString("0x000000000000000000000000000000000000c0de"), GWei.of(11)));
assertThat(V.validateWithdrawals(Optional.of(injected))).withFailMessage("a non-empty withdrawal list was accepted").isFalse();
final BlockDataGenerator gen = new BlockDataGenerator();
final Block withRoot =
gen.block(BlockDataGenerator.BlockOptions.create().setWithdrawals(Optional.of(injected)).setWithdrawalsRoot(Hash.ZERO));
assertThat(V.validateWithdrawalsRoot(withRoot)).withFailMessage("a withdrawals root other than the empty trie was accepted").isFalse();
// upstream b7f1387cb4: a root in the header with NO withdrawals field in the body is malformed, even when the root is the empty one
final Block noBody = gen.block(BlockDataGenerator.BlockOptions.create().setWithdrawals(Optional.empty()).setWithdrawalsRoot(Hash.EMPTY_TRIE_HASH));
assertThat(V.validateWithdrawalsRoot(noBody)).withFailMessage("a header root without body withdrawals was accepted").isFalse();
}
@Test
void theEmptyFormsAreStillAccepted() {
// positive control: the blocks the chain actually carries (empty list, empty-trie root) and pre-Shanghai (absent)
assertThat(V.validateWithdrawals(Optional.of(Collections.emptyList()))).isTrue();
assertThat(V.validateWithdrawals(Optional.empty())).isTrue();
final BlockDataGenerator gen = new BlockDataGenerator();
assertThat(
V.validateWithdrawalsRoot(
gen.block(
BlockDataGenerator.BlockOptions.create()
.setWithdrawals(Optional.of(Collections.emptyList()))
.setWithdrawalsRoot(Hash.EMPTY_TRIE_HASH))))
.isTrue();
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.feemarket;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.feemarket.TransactionPriceCalculator;
import java.util.Optional;
import org.junit.jupiter.api.Test;
/**
* Tests for the AERE fork-gated base-fee floor in {@link LondonFeeMarket}.
*
* <p>Proves: (a) fork UNSET is byte-behaviour-identical to upstream London (empty blocks decay to /
* stay at the legacy 7-wei floor, no 1-Gwei clamp); (b) once the fork block N is set with a 1-Gwei
* floor, empty blocks at/after N floor at EXACTLY 1 Gwei and never below; (c) the jump to >= 1 Gwei
* happens exactly at block N (block N-1 is still legacy); (d) an over-target block still rises above
* 1 Gwei via the normal EIP-1559 formula.
*/
public class LondonFeeMarketAereFloorTest {
private static final long ONE_GWEI = 1_000_000_000L;
private static final long GAS_TARGET = 15_000_000L;
private static final long FORK_BLOCK = 100L;
/** London fee market with an explicit AERE floor-fork configuration (bypasses global JVM config). */
private static LondonFeeMarket market(final long aereForkBlock, final long aereFloorValueWei) {
return new LondonFeeMarket(
TransactionPriceCalculator.eip1559(),
0L, // londonForkBlockNumber (kept off the tested block range)
Optional.empty(),
aereForkBlock,
aereFloorValueWei);
}
// (a) UNSET fork => byte-identical to upstream: empty blocks decay to and stay at 7 wei, and no
// value is ever clamped up to 1 Gwei.
@Test
public void forkUnset_emptyBlocksDecayToSevenWei_noClampToOneGwei() {
final LondonFeeMarket unset = market(Long.MAX_VALUE, ONE_GWEI);
// Decay many empty blocks from well above the floor: must settle at EXACTLY 7 wei.
Wei baseFee = Wei.of(10_000L);
for (int i = 0; i < 400; i++) {
baseFee = unset.computeBaseFee(500L + i, baseFee, 0L, GAS_TARGET);
}
assertThat(baseFee).isEqualTo(Wei.of(7L));
// Once at 7 wei it stays at 7 wei (never below), never jumps to 1 Gwei.
assertThat(unset.computeBaseFee(9_000L, Wei.of(7L), 0L, GAS_TARGET)).isEqualTo(Wei.of(7L));
// A mid-range empty-block value is NOT clamped up (proves the fork is a genuine no-op):
// 1000 - 1000/8 = 1000 - 125 = 875 wei.
assertThat(unset.computeBaseFee(9_000L, Wei.of(1_000L), 0L, GAS_TARGET))
.isEqualTo(Wei.of(875L));
}
// (b) Fork ACTIVE => empty blocks at/after N floor at EXACTLY 1 Gwei and never drop below.
@Test
public void forkActive_emptyBlocksFloorAtExactlyOneGwei_neverBelow() {
final LondonFeeMarket forked = market(FORK_BLOCK, ONE_GWEI);
// Sanity: raw EIP-1559 decrease from 1 Gwei on an empty block would be 875M; clamp pins it.
assertThat(forked.computeBaseFee(FORK_BLOCK + 1, Wei.of(ONE_GWEI), 0L, GAS_TARGET))
.isEqualTo(Wei.of(ONE_GWEI));
// Many consecutive empty blocks after the fork: stays EXACTLY at 1 Gwei each block.
Wei baseFee = Wei.of(ONE_GWEI);
for (int i = 0; i < 50; i++) {
baseFee = forked.computeBaseFee(FORK_BLOCK + 1 + i, baseFee, 0L, GAS_TARGET);
assertThat(baseFee).isEqualTo(Wei.of(ONE_GWEI));
}
// Even a pathologically low parent baseFee (7 wei) is clamped up to exactly 1 Gwei post-fork.
assertThat(forked.computeBaseFee(FORK_BLOCK + 500, Wei.of(7L), 0L, GAS_TARGET))
.isEqualTo(Wei.of(ONE_GWEI));
}
// (c) The jump to >= 1 Gwei happens exactly at the fork block N (N-1 is still legacy).
@Test
public void forkActivation_jumpHappensExactlyAtForkBlock() {
final LondonFeeMarket forked = market(FORK_BLOCK, ONE_GWEI);
// Block N-1 is pre-fork: legacy behaviour, a 7-wei empty-block parent stays at 7 wei.
assertThat(forked.computeBaseFee(FORK_BLOCK - 1, Wei.of(7L), 0L, GAS_TARGET))
.isEqualTo(Wei.of(7L));
// Block N itself jumps to >= 1 Gwei even though the parent baseFee was only 7 wei.
final Wei atForkBlock = forked.computeBaseFee(FORK_BLOCK, Wei.of(7L), 0L, GAS_TARGET);
assertThat(atForkBlock).isEqualTo(Wei.of(ONE_GWEI));
assertThat(atForkBlock.greaterOrEqualThan(Wei.of(ONE_GWEI))).isTrue();
}
// (d) An over-target (active) block still rises above 1 Gwei via the normal EIP-1559 formula,
// and the clamp does not distort above-floor values (identical to the unset market's raw result).
@Test
public void forkActive_overTargetBlockRisesAboveOneGweiNormally() {
final LondonFeeMarket forked = market(FORK_BLOCK, ONE_GWEI);
final LondonFeeMarket unset = market(Long.MAX_VALUE, ONE_GWEI);
final long parentGasUsed = 2 * GAS_TARGET; // over target => baseFee increases
final Wei parent = Wei.of(ONE_GWEI);
// EIP-1559 rise: 1Gwei + 1Gwei * (2T - T)/T / 8 = 1Gwei + 1Gwei/8 = 1_125_000_000 wei.
final Wei rose = forked.computeBaseFee(FORK_BLOCK + 5, parent, parentGasUsed, GAS_TARGET);
assertThat(rose).isEqualTo(Wei.of(1_125_000_000L));
assertThat(rose.greaterOrEqualThan(Wei.of(ONE_GWEI))).isTrue();
assertThat(rose).isNotEqualTo(Wei.of(ONE_GWEI)); // strictly above the floor
// Above-floor values are unaffected by the clamp: identical to the unset (no-op) market.
assertThat(rose)
.isEqualTo(unset.computeBaseFee(FORK_BLOCK + 5, parent, parentGasUsed, GAS_TARGET));
}
}

View File

@ -0,0 +1,620 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.parallelization;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.MINING_BENEFICIARY;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.hyperledger.besu.config.GenesisConfig;
import org.hyperledger.besu.config.JsonUtil;
import org.hyperledger.besu.crypto.KeyPair;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.BlockProcessingResult;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockBody;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
import org.hyperledger.besu.ethereum.core.ExecutionContextTestFixture;
import org.hyperledger.besu.ethereum.core.MutableWorldState;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.Util;
import org.hyperledger.besu.ethereum.mainnet.BlockProcessor;
import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.mainnet.blockstm.AereBlockStmEngine;
import org.hyperledger.besu.ethereum.mainnet.blockstm.AereBlockStmPreprocessing;
import org.hyperledger.besu.ethereum.mainnet.blockstm.BlockStmStats;
import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams;
import org.hyperledger.besu.evm.blockhash.BlockHashLookup;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.plugin.services.storage.DataStorageFormat;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
/**
* AERE Block-STM benchmark + verification harness (chain-independent, block-processor grain).
*
* <p>Proves serial-equivalence (parallel state root == sequential state root) for an independent
* workload AND a genuine read/write-conflict workload (all transfers to one shared recipient, which
* forces the multi-version scheduler to abort and re-execute), and measures parallel-vs-sequential
* block execution time / TPS across worker counts. All numbers are written to {@code REPORT}.
*/
public class AereBlockStmBenchmarkTest extends AbstractParallelBlockProcessorIntegrationTest {
private static final String BASE_GENESIS =
"/org/hyperledger/besu/ethereum/mainnet/parallelization/genesis-it.json";
private static final Path REPORT =
Path.of(System.getProperty("java.io.tmpdir"), "blockstm_bench_results.txt");
// Compute-heavy runtime: PUSH1 0 CALLDATALOAD (iters); loop { KECCAK256 32 bytes of mem; MSTORE };
// then SSTORE slot0=1; STOP. Iteration count is supplied per-tx via 32-byte calldata.
private static final String COMPUTE_CODE =
"0x6000355b60206000206000526001900380600357600160005500";
private static final int MAX_SENDERS = 1024;
private static final Wei BASE_FEE = Wei.of(5);
private static GenesisConfig customGenesis;
private static KeyPair[] senders;
private static Address[] senderAddr;
private final List<String> lines = new ArrayList<>();
private static Address recipient(final int i) {
return Address.fromHexString(String.format("0x%040x", 0x10000000L + i));
}
private static final Address SHARED_RECIPIENT =
Address.fromHexString(String.format("0x%040x", 0x0DEAD001L));
private static Address computeContract(final int i) {
return Address.fromHexString(String.format("0x%040x", 0x20000000L + i));
}
@BeforeAll
static void buildGenesis() throws Exception {
final String baseText;
try (InputStream is = AereBlockStmBenchmarkTest.class.getResourceAsStream(BASE_GENESIS)) {
baseText = new String(is.readAllBytes(), StandardCharsets.UTF_8);
}
final ObjectNode root = JsonUtil.objectNodeFromString(baseText);
final ObjectNode alloc = (ObjectNode) root.get("alloc");
senders = new KeyPair[MAX_SENDERS];
senderAddr = new Address[MAX_SENDERS];
final String richBalance = "0x" + new BigInteger("1000000000000000000000000").toString(16);
for (int i = 0; i < MAX_SENDERS; i++) {
final String pkHex = String.format("%064x", i + 1);
final KeyPair kp = ParallelBlockProcessorTestSupport.generateKeyPair(pkHex);
senders[i] = kp;
senderAddr[i] = Util.publicKeyToAddress(kp.getPublicKey());
final ObjectNode acct = JsonUtil.createEmptyObjectNode();
acct.put("balance", richBalance);
alloc.set(senderAddr[i].getBytes().toUnprefixedHexString(), acct);
final ObjectNode contract = JsonUtil.createEmptyObjectNode();
contract.put("balance", "0");
contract.put("code", COMPUTE_CODE);
alloc.set(computeContract(i).getBytes().toUnprefixedHexString(), contract);
}
customGenesis = GenesisConfig.fromConfig(root);
Files.write(
REPORT,
("AERE BLOCK-STM BENCHMARK / VERIFICATION REPORT\n"
+ "generated on a scratch host (chain-independent block-processor grain)\n"
+ "cpus(reported)=" + Runtime.getRuntime().availableProcessors()
+ " loadAvg(start)="
+ String.format("%.2f", ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage())
+ "\n===============================================================\n")
.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
private void log(final String s) {
lines.add(s);
System.out.println("[BLOCKSTM-BENCH] " + s);
}
private void flush() throws Exception {
Files.write(
REPORT,
(String.join("\n", lines) + "\n").getBytes(StandardCharsets.UTF_8),
StandardOpenOption.APPEND);
lines.clear();
}
@Override
protected ExecutionContextTestFixture createFreshContext() {
return ExecutionContextTestFixture.builder(customGenesis)
.dataStorageFormat(DataStorageFormat.BONSAI)
.build();
}
@Override
protected String getVariantName() {
return "AERE Block-STM Benchmark";
}
@Override
protected ParallelTransactionPreprocessing createParallelPreprocessing(
final MainnetTransactionProcessor transactionProcessor) {
return new ParallelTransactionPreprocessing(
transactionProcessor, Runnable::run, getBalConfiguration());
}
// ---- workloads ----------------------------------------------------------
private Transaction[] independentTransfers(final int n) {
final Transaction[] txs = new Transaction[n];
for (int i = 0; i < n; i++) {
txs[i] =
createTransferTransaction(
0, 1L, 100_000L, 0L, 5L, recipient(i).toHexString(), senders[i]);
}
return txs;
}
private Transaction[] lowConflictTransfers(final int n, final int pool) {
// Distinct senders credit recipients drawn from a small pool -> occasional write-conflict on a
// shared recipient balance (ERC20-style low contention). pool<n means ~ (n-pool)/n of txs share.
final int p = Math.max(1, pool);
final Transaction[] txs = new Transaction[n];
for (int i = 0; i < n; i++) {
txs[i] =
createTransferTransaction(
0, 1L, 100_000L, 0L, 5L, recipient(i % p).toHexString(), senders[i]);
}
return txs;
}
private Transaction[] conflictingTransfers(final int n) {
// Every distinct sender credits ONE shared recipient -> read/write conflict on that balance.
final Transaction[] txs = new Transaction[n];
for (int i = 0; i < n; i++) {
txs[i] =
createTransferTransaction(
0, 1L, 100_000L, 0L, 5L, SHARED_RECIPIENT.toHexString(), senders[i]);
}
return txs;
}
private Transaction[] independentCompute(final int n, final long iters) {
final Bytes payload = UInt256.valueOf(iters).toBytes();
final Transaction[] txs = new Transaction[n];
for (int i = 0; i < n; i++) {
txs[i] =
Transaction.builder()
.type(TransactionType.EIP1559)
.nonce(0)
.maxPriorityFeePerGas(Wei.ZERO)
.maxFeePerGas(Wei.of(5))
.gasLimit(30_000_000L)
.to(computeContract(i))
.value(Wei.ZERO)
.payload(payload)
.chainId(BigInteger.valueOf(42))
.signAndBuild(senders[i]);
}
return txs;
}
// ---- block plumbing (configurable gas limit) ----------------------------
private Block buildBlock(
final BlockHeader parent,
final Hash stateRoot,
final long gasLimit,
final Transaction[] txs) {
final BlockHeader header =
new BlockHeaderTestFixture()
.number(parent.getNumber() + 1L)
.parentHash(parent.getHash())
.coinbase(MINING_BENEFICIARY)
.stateRoot(stateRoot)
.gasLimit(gasLimit)
.baseFeePerGas(BASE_FEE)
.buildHeader();
final BlockBody body =
new BlockBody(Arrays.asList(txs), Collections.emptyList(), Optional.empty());
return new Block(header, body);
}
private Hash discover(final long gasLimit, final Transaction[] txs) {
final ExecutionContextTestFixture ctx = createFreshContext();
final BlockHeader parent = ctx.getBlockchain().getChainHeadHeader();
final MutableWorldState ws =
ctx.getStateArchive()
.getWorldState(WorldStateQueryParams.withBlockHeaderAndUpdateNodeHead(parent))
.orElseThrow();
final Block block = buildBlock(parent, Hash.ZERO, gasLimit, txs);
final BlockProcessor seq = createSequentialProcessor(ctx);
final BlockProcessingResult r =
seq.processBlock(ctx.getProtocolContext(), ctx.getBlockchain(), ws, block);
if (r.isSuccessful()) {
return ws.rootHash();
}
// Header stateRoot was ZERO, so processing "failed" on the root-mismatch check; recover the
// real (calculated) root from the error message: the first non-zero 32-byte hex it contains.
final String msg = r.errorMessage.orElseThrow();
final java.util.regex.Matcher m =
java.util.regex.Pattern.compile("0x[0-9a-fA-F]{64}").matcher(msg);
while (m.find()) {
final Hash h = Hash.fromHexString(m.group());
if (!h.equals(Hash.ZERO)) {
return h;
}
}
throw new AssertionError("could not recover state root from: " + msg);
}
private BlockProcessor stockParallel(final ExecutionContextTestFixture ctx) {
final ProtocolSpec spec =
ctx.getProtocolSchedule()
.getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader());
return new MainnetParallelBlockProcessor(
spec.getTransactionProcessor(),
spec.getTransactionReceiptFactory(),
Wei.ZERO,
BlockHeader::getCoinbase,
true,
ctx.getProtocolSchedule(),
getBalConfiguration(),
new NoOpMetricsSystem());
}
/** One timed sequential run. Returns [rootHex, nanos]. */
private Object[] runSeq(final long gasLimit, final Hash stateRoot, final Transaction[] txs) {
final ExecutionContextTestFixture ctx = createFreshContext();
final BlockHeader parent = ctx.getBlockchain().getChainHeadHeader();
final MutableWorldState ws =
ctx.getStateArchive()
.getWorldState(WorldStateQueryParams.withBlockHeaderAndUpdateNodeHead(parent))
.orElseThrow();
final Block block = buildBlock(parent, stateRoot, gasLimit, txs);
final BlockProcessor seq = createSequentialProcessor(ctx);
final long t0 = System.nanoTime();
final BlockProcessingResult r =
seq.processBlock(ctx.getProtocolContext(), ctx.getBlockchain(), ws, block);
final long dt = System.nanoTime() - t0;
assertTrue(r.isSuccessful(), "seq failed: " + r.errorMessage.orElse("?"));
return new Object[] {ws.rootHash(), dt, r};
}
/** One timed AERE Block-STM parallel run at {@code workers}. Returns [rootHex, nanos, nbParallel]. */
private Object[] runPar(
final long gasLimit, final Hash stateRoot, final int workers, final Transaction[] txs) {
final ExecutionContextTestFixture ctx = createFreshContext();
final BlockHeader parent = ctx.getBlockchain().getChainHeadHeader();
final MutableWorldState ws =
ctx.getStateArchive()
.getWorldState(WorldStateQueryParams.withBlockHeaderAndUpdateNodeHead(parent))
.orElseThrow();
final Block block = buildBlock(parent, stateRoot, gasLimit, txs);
final ProtocolSpec spec =
ctx.getProtocolSchedule()
.getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader());
final BlockProcessor par = stockParallel(ctx);
final AereBlockStmPreprocessing pf =
new AereBlockStmPreprocessing(spec.getTransactionProcessor(), workers);
final long t0 = System.nanoTime();
final BlockProcessingResult r =
par.processBlock(ctx.getProtocolContext(), ctx.getBlockchain(), ws, block, pf);
final long dt = System.nanoTime() - t0;
assertTrue(r.isSuccessful(), "par failed: " + r.errorMessage.orElse("?"));
return new Object[] {ws.rootHash(), dt, r.getNbParallelizedTransactions().orElse(0)};
}
/** Drive the engine directly to read scheduler statistics (executions / aborts / verified). */
private BlockStmStats engineStats(final long gasLimit, final int workers, final Transaction[] txs) {
final ExecutionContextTestFixture ctx = createFreshContext();
final BlockHeader parent = ctx.getBlockchain().getChainHeadHeader();
final Block block = buildBlock(parent, Hash.ZERO, gasLimit, txs);
final AereBlockStmEngine engine =
new AereBlockStmEngine(
ctx.getProtocolSchedule()
.getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader())
.getTransactionProcessor(),
workers);
final BlockHashLookup zero = (frame, number) -> Hash.ZERO;
engine.runAsyncBlock(
ctx.getProtocolContext(),
block.getHeader(),
Arrays.asList(txs),
MINING_BENEFICIARY,
zero,
Wei.ZERO,
Runnable::run,
Optional.empty(),
Optional.of(parent));
return engine.stats();
}
private static long median(final long[] a) {
final long[] c = a.clone();
Arrays.sort(c);
return c[c.length / 2];
}
// ---- the suite ----------------------------------------------------------
@Test
void blockStmVerifyAndBenchmark() throws Exception {
final int cpus = Runtime.getRuntime().availableProcessors();
final int[] workerCounts = {1, 2, 4, 8, 16};
// ===================== CORRECTNESS =====================
log("");
log("## PART 1 - CORRECTNESS (parallel root == sequential root)");
// 1a. Independent transfers: expect zero aborts, executions == n.
{
final int n = 300;
final Transaction[] txs = independentTransfers(n);
final Hash root = discover(30_000_000L, txs);
final Object[] seq = runSeq(30_000_000L, root, txs);
final Object[] par = runPar(30_000_000L, root, cpus, txs);
final Hash seqRoot = (Hash) seq[0];
final Hash parRoot = (Hash) par[0];
final int nbPar = (Integer) par[2];
final BlockStmStats st = engineStats(30_000_000L, cpus, txs);
assertThat(parRoot).as("independent parallel root == sequential").isEqualTo(seqRoot);
log(String.format(
" [independent n=%d w=%d] parRoot==seqRoot: %s root=%s", n, cpus,
parRoot.equals(seqRoot), seqRoot.toHexString()));
log(String.format(
" nbParallelized=%d verified=%s execs=%d (n=%d) aborts=%d committedParallel=%d",
nbPar, st.verified(), st.totalExecutions(), st.numTxns(), st.totalAborts(),
st.committedParallel()));
assertThat(st.verified()).as("independent must verify").isTrue();
assertThat(st.totalAborts()).as("independent must have zero aborts").isZero();
assertThat(st.totalExecutions()).as("independent: one execution per tx").isEqualTo(n);
assertThat(nbPar).as("parallel path exercised").isGreaterThan(0);
}
// 1b. Conflicting transfers (shared recipient): expect aborts>0, executions>n, root still equal.
{
final int n = 300;
final Transaction[] txs = conflictingTransfers(n);
final Hash root = discover(30_000_000L, txs);
final Object[] seq = runSeq(30_000_000L, root, txs);
final Object[] par = runPar(30_000_000L, root, cpus, txs);
final Hash seqRoot = (Hash) seq[0];
final Hash parRoot = (Hash) par[0];
final int nbPar = (Integer) par[2];
final BlockStmStats st = engineStats(30_000_000L, cpus, txs);
assertThat(parRoot).as("CONFLICT parallel root == sequential").isEqualTo(seqRoot);
log(String.format(
" [conflict n=%d w=%d] parRoot==seqRoot: %s root=%s", n, cpus,
parRoot.equals(seqRoot), seqRoot.toHexString()));
log(String.format(
" nbParallelized=%d verified=%s execs=%d (n=%d, re-exec=%d) aborts=%d committedParallel=%d",
nbPar, st.verified(), st.totalExecutions(), st.numTxns(),
st.totalExecutions() - st.numTxns(), st.totalAborts(), st.committedParallel()));
assertThat(st.verified()).as("conflict must verify").isTrue();
assertThat(st.totalAborts()).as("conflict must trigger re-execution (aborts>0)").isGreaterThan(0);
assertThat(st.totalExecutions()).as("conflict re-executes (execs>n)").isGreaterThan(n);
}
// 1c. Independent compute contract calls: root equal, zero aborts.
{
final int n = 128;
final long iters = 400;
final Transaction[] txs = independentCompute(n, iters);
final Hash root = discover(3_000_000_000L, txs);
final Object[] seq = runSeq(3_000_000_000L, root, txs);
final Object[] par = runPar(3_000_000_000L, root, cpus, txs);
final Hash seqRoot = (Hash) seq[0];
final Hash parRoot = (Hash) par[0];
final BlockStmStats st = engineStats(3_000_000_000L, cpus, txs);
assertThat(parRoot).as("compute parallel root == sequential").isEqualTo(seqRoot);
log(String.format(
" [compute n=%d w=%d iters=%d] parRoot==seqRoot: %s root=%s", n, cpus, iters,
parRoot.equals(seqRoot), seqRoot.toHexString()));
log(String.format(
" verified=%s execs=%d (n=%d) aborts=%d committedParallel=%d",
st.verified(), st.totalExecutions(), st.numTxns(), st.totalAborts(),
st.committedParallel()));
assertThat(st.verified()).isTrue();
assertThat(st.totalAborts()).isZero();
}
// 1d. Low-conflict transfers (recipient pool): root equal, verified (aborts allowed but rare).
{
final int n = 300;
final Transaction[] txs = lowConflictTransfers(n, 270);
final Hash root = discover(30_000_000L, txs);
final Object[] seq = runSeq(30_000_000L, root, txs);
final Object[] par = runPar(30_000_000L, root, cpus, txs);
final Hash seqRoot = (Hash) seq[0];
final Hash parRoot = (Hash) par[0];
final int nbPar = (Integer) par[2];
final BlockStmStats st = engineStats(30_000_000L, cpus, txs);
assertThat(parRoot).as("low-conflict parallel root == sequential").isEqualTo(seqRoot);
log(String.format(
" [low-conflict n=%d w=%d pool=270] parRoot==seqRoot: %s root=%s", n, cpus,
parRoot.equals(seqRoot), seqRoot.toHexString()));
log(String.format(
" nbParallelized=%d verified=%s execs=%d (n=%d, re-exec=%d) aborts=%d committedParallel=%d",
nbPar, st.verified(), st.totalExecutions(), st.numTxns(),
st.totalExecutions() - st.numTxns(), st.totalAborts(), st.committedParallel()));
assertThat(st.verified()).as("low-conflict must verify").isTrue();
}
flush();
// ===================== THROUGHPUT =====================
final int warmup = 3;
final int measure = 5;
// Workload A: independent transfers.
runThroughput(
"PART 2A - THROUGHPUT: independent transfers", 1000, 30_000_000L,
independentTransfers(1000), workerCounts, warmup, measure);
// Workload A': independent compute-heavy calls (execution-bound; best case for parallelism).
runThroughput(
"PART 2B - THROUGHPUT: independent compute calls (iters=800)", 256, 3_000_000_000L,
independentCompute(256, 800), workerCounts, warmup, measure);
// Workload C: high-conflict transfers (all to one recipient) - expect no speedup.
runThroughput(
"PART 2C - THROUGHPUT: high-conflict transfers (shared recipient)", 500, 30_000_000L,
conflictingTransfers(500), workerCounts, warmup, measure);
// Workload D: low-conflict transfers (recipient pool=900 of 1000) - ERC20-style ~10% contention.
runThroughput(
"PART 2D - THROUGHPUT: low-conflict transfers (recipient pool=900)", 1000, 30_000_000L,
lowConflictTransfers(1000, 900), workerCounts, warmup, measure);
// ===================== ISOLATED EXECUTION PHASE =====================
// The real Block-STM test: time ONLY the parallel execution phase (engine.runAsyncBlock, i.e.
// the flat-snapshot base reads + concurrent EVM execution + MV bookkeeping), measured INSIDE the
// engine (execPhaseNanos), with the serial Bonsai trie commit excluded entirely. Execution-bound
// independent compute calls so there is real work to parallelize; scaling vs w=1 is the proof.
runExecPhase(
"PART 3A - EXECUTION PHASE (isolated): independent compute calls (iters=800)",
256, 3_000_000_000L, independentCompute(256, 800), workerCounts, warmup, measure);
runExecPhase(
"PART 3B - EXECUTION PHASE (isolated): independent compute calls (iters=3000, heavy)",
256, 3_000_000_000L, independentCompute(256, 3000), workerCounts, warmup, measure);
log("");
log("loadAvg(end)="
+ String.format("%.2f", ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage()));
log("=============================== END ===============================");
flush();
}
private void runThroughput(
final String title,
final int n,
final long gasLimit,
final Transaction[] txs,
final int[] workerCounts,
final int warmup,
final int measure)
throws Exception {
log("");
log("## " + title + " (n=" + n + " txs)");
final Hash root = discover(gasLimit, txs);
// Sequential baseline.
for (int i = 0; i < warmup; i++) {
runSeq(gasLimit, root, txs);
}
final long[] seqT = new long[measure];
for (int i = 0; i < measure; i++) {
seqT[i] = (Long) runSeq(gasLimit, root, txs)[1];
}
final long seqMed = median(seqT);
final double seqMs = seqMed / 1e6;
final double seqTps = n / (seqMed / 1e9);
log(String.format(
" SEQUENTIAL (MainnetBlockProcessor): %8.3f ms %10.0f tx/s", seqMs, seqTps));
log(" worker | median ms | tx/s | speedup vs seq | scaling vs w=1");
log(" -------+------------+----------+----------------+---------------");
double par1Ms = Double.NaN;
for (final int w : workerCounts) {
for (int i = 0; i < warmup; i++) {
runPar(gasLimit, root, w, txs);
}
final long[] pt = new long[measure];
int nbPar = 0;
for (int i = 0; i < measure; i++) {
final Object[] r = runPar(gasLimit, root, w, txs);
pt[i] = (Long) r[1];
nbPar = (Integer) r[2];
}
final long med = median(pt);
final double ms = med / 1e6;
final double tps = n / (med / 1e9);
if (w == 1) {
par1Ms = ms;
}
log(String.format(
" %6d | %10.3f | %8.0f | %13.2fx | %12.2fx (nbParallel=%d)",
w, ms, tps, seqMs / ms, par1Ms / ms, nbPar));
}
flush();
}
/**
* Measures the ISOLATED parallel execution phase across worker counts. Drives the engine directly
* (runAsyncBlock) and reads {@code execPhaseNanos} recorded INSIDE the engine (worker pool start ->
* termination), so the flat-snapshot mint and the downstream serial Bonsai trie commit are both
* excluded. Scaling vs w=1 of this number is the direct test of whether Block-STM execution
* parallelism works once the per-worker snapshot overhead is removed.
*/
private void runExecPhase(
final String title,
final int n,
final long gasLimit,
final Transaction[] txs,
final int[] workerCounts,
final int warmup,
final int measure)
throws Exception {
log("");
log("## " + title + " (n=" + n + " txs)");
log(" worker | exec-phase ms | snapshot ms | exec tx/s | scaling vs w=1 | verified");
log(" -------+----------------+-------------+--------------+----------------+---------");
double w1Ms = Double.NaN;
for (final int w : workerCounts) {
for (int i = 0; i < warmup; i++) {
engineStats(gasLimit, w, txs);
}
final long[] execT = new long[measure];
final long[] snapT = new long[measure];
boolean verified = true;
for (int i = 0; i < measure; i++) {
final BlockStmStats st = engineStats(gasLimit, w, txs);
execT[i] = st.execPhaseNanos();
snapT[i] = st.snapshotNanos();
verified = verified && st.verified();
}
final long execMed = median(execT);
final long snapMed = median(snapT);
final double execMs = execMed / 1e6;
final double snapMs = snapMed / 1e6;
final double tps = n / (execMed / 1e9);
if (w == 1) {
w1Ms = execMs;
}
assertThat(verified).as("exec-phase run must verify at w=" + w).isTrue();
log(String.format(
" %6d | %14.3f | %11.3f | %12.0f | %13.2fx | %s",
w, execMs, snapMs, tps, w1Ms / execMs, verified));
}
flush();
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright contributors to AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.mainnet.parallelization;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_2;
import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_3;
import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_4;
import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_GENESIS_1_KEYPAIR;
import static org.hyperledger.besu.ethereum.mainnet.parallelization.ParallelBlockProcessorTestSupport.ACCOUNT_GENESIS_2_KEYPAIR;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.BlockProcessingResult;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
import org.hyperledger.besu.ethereum.core.ExecutionContextTestFixture;
import org.hyperledger.besu.ethereum.core.MutableWorldState;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.AbstractBlockProcessor.PreprocessingFunction;
import org.hyperledger.besu.ethereum.mainnet.BlockProcessor;
import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.mainnet.blockstm.AereBlockStmPreprocessing;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Smoke test proving AERE Block-STM parallel execution is serial-equivalent: a block processed
* through the AERE Block-STM engine (a stock {@link MainnetParallelBlockProcessor} driven by {@link
* AereBlockStmPreprocessing}) yields a state root bit-identical to sequential {@code
* MainnetBlockProcessor} execution of the same block, and the parallel path is actually exercised.
*/
public class AereBlockStmSmokeTest extends AbstractParallelBlockProcessorIntegrationTest {
@Override
protected String getVariantName() {
return "AERE Block-STM";
}
@Override
protected ParallelTransactionPreprocessing createParallelPreprocessing(
final MainnetTransactionProcessor transactionProcessor) {
// Not used by this test; provided to satisfy the abstract base.
return new ParallelTransactionPreprocessing(
transactionProcessor, Runnable::run, getBalConfiguration());
}
private BlockProcessor stockParallelProcessor(final ExecutionContextTestFixture ctx) {
final ProtocolSpec spec =
ctx.getProtocolSchedule()
.getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader());
return new MainnetParallelBlockProcessor(
spec.getTransactionProcessor(),
spec.getTransactionReceiptFactory(),
Wei.ZERO,
BlockHeader::getCoinbase,
true,
ctx.getProtocolSchedule(),
getBalConfiguration(),
new NoOpMetricsSystem());
}
@Test
@DisplayName("AERE Block-STM parallel root equals sequential root for independent transfers")
void blockStmRootMatchesSequential() {
final Transaction tx1 =
createTransferTransaction(
0, 1_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_2, ACCOUNT_GENESIS_1_KEYPAIR);
final Transaction tx2 =
createTransferTransaction(
0, 2_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_3, ACCOUNT_GENESIS_2_KEYPAIR);
final Transaction tx3 =
createTransferTransaction(
1, 3_000_000_000_000_000_000L, 300_000L, 0L, 5L, ACCOUNT_4, ACCOUNT_GENESIS_1_KEYPAIR);
final Wei baseFee = Wei.of(5);
final Transaction[] txs = {tx1, tx2, tx3};
final Hash stateRoot = discoverStateRoot(baseFee, txs);
// Sequential
final ExecutionContextTestFixture seqCtx = createFreshContext();
final MutableWorldState seqWs = seqCtx.getStateArchive().getWorldState();
final Block seqBlock = createBlock(seqCtx, stateRoot, baseFee, txs);
final BlockProcessor seqProcessor = createSequentialProcessor(seqCtx);
final BlockProcessingResult seqResult =
seqProcessor.processBlock(
seqCtx.getProtocolContext(), seqCtx.getBlockchain(), seqWs, seqBlock);
assertTrue(seqResult.isSuccessful(), "sequential processing failed");
// Parallel via AERE Block-STM
final ExecutionContextTestFixture parCtx = createFreshContext();
final MutableWorldState parWs = parCtx.getStateArchive().getWorldState();
final Block parBlock = createBlock(parCtx, stateRoot, baseFee, txs);
final ProtocolSpec spec =
parCtx
.getProtocolSchedule()
.getByBlockHeader(new BlockHeaderTestFixture().number(0L).buildHeader());
final BlockProcessor parProcessor = stockParallelProcessor(parCtx);
final PreprocessingFunction blockStm =
new AereBlockStmPreprocessing(
spec.getTransactionProcessor(), Runtime.getRuntime().availableProcessors());
final BlockProcessingResult parResult =
parProcessor.processBlock(
parCtx.getProtocolContext(), parCtx.getBlockchain(), parWs, parBlock, blockStm);
assertTrue(
parResult.isSuccessful(),
"AERE Block-STM processing failed: " + parResult.errorMessage.orElse("(no message)"));
// Serial-equivalence: bit-identical 32-byte state root.
assertThat(parWs.rootHash())
.as("AERE Block-STM parallel state root must equal sequential")
.isEqualTo(seqWs.rootHash());
// The parallel path was actually exercised (outcomes consumed, not per-tx sequential fallback).
assertParallelizationRecordedInResults(seqResult, parResult, txs.length);
}
}

View File

@ -0,0 +1,941 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-cap-prestatus-rlpx-connections.patch.
*/
package org.hyperledger.besu.ethereum.eth.manager;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.eth.EthProtocol;
import org.hyperledger.besu.ethereum.eth.SnapProtocol;
import org.hyperledger.besu.ethereum.eth.manager.EthPeer.DisconnectCallback;
import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerSelector;
import org.hyperledger.besu.ethereum.eth.peervalidation.PeerValidator;
import org.hyperledger.besu.ethereum.eth.sync.ChainHeadTracker;
import org.hyperledger.besu.ethereum.eth.sync.SnapServerChecker;
import org.hyperledger.besu.ethereum.eth.sync.SyncMode;
import org.hyperledger.besu.ethereum.eth.sync.TrailingPeerRequirements;
import org.hyperledger.besu.ethereum.forkid.ForkId;
import org.hyperledger.besu.ethereum.forkid.ForkIdManager;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.p2p.peers.Peer;
import org.hyperledger.besu.ethereum.p2p.peers.PeerId;
import org.hyperledger.besu.ethereum.p2p.rlpx.RlpxAgent;
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.PeerClientName;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.PeerInfo;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.Counter;
import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric;
import org.hyperledger.besu.plugin.services.permissioning.NodeMessagePermissioningProvider;
import org.hyperledger.besu.util.Subscribers;
import java.time.Clock;
import java.time.Duration;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalNotification;
import jakarta.validation.constraints.NotNull;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EthPeers implements PeerSelector {
private static final Logger LOG = LoggerFactory.getLogger(EthPeers.class);
public static final Comparator<EthPeerImmutableAttributes> TOTAL_DIFFICULTY =
Comparator.comparing((final EthPeerImmutableAttributes p) -> p.estimatedTotalDifficulty());
public static final Comparator<EthPeerImmutableAttributes> CHAIN_HEIGHT =
Comparator.comparing((final EthPeerImmutableAttributes p) -> p.estimatedChainHeight());
public static final Comparator<EthPeerImmutableAttributes> MOST_USEFUL_PEER =
Comparator.comparing((final EthPeerImmutableAttributes p) -> p.reputationScore())
.thenComparing(CHAIN_HEIGHT);
public static final Comparator<EthPeerImmutableAttributes> TOTAL_DIFFICULTY_THEN_HEIGHT =
TOTAL_DIFFICULTY.thenComparing(CHAIN_HEIGHT);
public static final Comparator<EthPeerImmutableAttributes> LEAST_TO_MOST_BUSY =
Comparator.comparing(EthPeerImmutableAttributes::outstandingRequests)
.thenComparing(EthPeerImmutableAttributes::lastRequestTimestamp);
public static final int NODE_ID_LENGTH = 64;
public static final int USEFULL_PEER_SCORE_THRESHOLD = 102;
private final Map<Bytes, EthPeer> activeConnections = new ConcurrentHashMap<>();
/**
* Lower bound for the pre-STATUS (incomplete) connection cap, so that even very small {@code
* --max-peers} values still tolerate a reasonable number of concurrent inbound handshakes. AERE
* D-434: ported from upstream hyperledger/besu e540ae2394 (GHSA-6r9q-wjp4-34gh).
*/
private static final int INCOMPLETE_CONNECTIONS_CAP_FLOOR = 10;
/**
* Maximum number of connections that have completed the devp2p HELLO but not yet the eth STATUS
* handshake that we retain. Bounds file-descriptor and heap growth from peers that connect and
* never send STATUS, which are otherwise invisible to {@code --max-peers} accounting.
*/
private final int maxIncompleteConnections;
private final Cache<PeerConnection, EthPeer> incompleteConnections;
private final Clock clock;
private final List<NodeMessagePermissioningProvider> permissioningProviders;
private final int maxMessageSize;
private final Subscribers<ConnectCallback> connectCallbacks = Subscribers.create();
private final Subscribers<DisconnectCallback> disconnectCallbacks = Subscribers.create();
private final Collection<PendingPeerRequest> pendingRequests = new CopyOnWriteArrayList<>();
private final int peerUpperBound;
private final int maxRemotelyInitiatedConnections;
private final Boolean randomPeerPriority;
private final Bytes nodeIdMask = Bytes.random(NODE_ID_LENGTH);
private final Supplier<ProtocolSpec> currentProtocolSpecSupplier;
private final SyncMode syncMode;
private final ForkIdManager forkIdManager;
private final int snapServerTargetNumber;
private final boolean shouldLimitRemoteConnections;
private Comparator<EthPeerImmutableAttributes> bestPeerComparator;
private final Bytes localNodeId;
private RlpxAgent rlpxAgent;
private final Counter connectedPeersCounter;
private ChainHeadTracker tracker;
private SnapServerChecker snapServerChecker;
private boolean snapServerPeersNeeded = false;
private Supplier<TrailingPeerRequirements> trailingPeerRequirementsSupplier =
() -> TrailingPeerRequirements.UNRESTRICTED;
public EthPeers(
final Supplier<ProtocolSpec> currentProtocolSpecSupplier,
final Clock clock,
final MetricsSystem metricsSystem,
final int maxMessageSize,
final List<NodeMessagePermissioningProvider> permissioningProviders,
final Bytes localNodeId,
final int peerUpperBound,
final int maxRemotelyInitiatedConnections,
final Boolean randomPeerPriority,
final SyncMode syncMode,
final ForkIdManager forkIdManager) {
this.currentProtocolSpecSupplier = currentProtocolSpecSupplier;
this.clock = clock;
this.permissioningProviders = permissioningProviders;
this.maxMessageSize = maxMessageSize;
this.bestPeerComparator = TOTAL_DIFFICULTY_THEN_HEIGHT;
this.localNodeId = localNodeId;
this.peerUpperBound = peerUpperBound;
this.maxRemotelyInitiatedConnections = maxRemotelyInitiatedConnections;
this.randomPeerPriority = randomPeerPriority;
LOG.trace("MaxPeers: {}, Max Remote: {}", peerUpperBound, maxRemotelyInitiatedConnections);
this.syncMode = syncMode;
this.forkIdManager = forkIdManager;
this.snapServerTargetNumber =
peerUpperBound / 2; // 50% of peers should be snap servers while snap syncing
this.shouldLimitRemoteConnections = maxRemotelyInitiatedConnections < peerUpperBound;
this.maxIncompleteConnections = Math.max(peerUpperBound * 2, INCOMPLETE_CONNECTIONS_CAP_FLOOR);
this.incompleteConnections =
CacheBuilder.newBuilder()
.maximumSize(maxIncompleteConnections)
.expireAfterWrite(Duration.ofSeconds(20L))
.concurrencyLevel(1)
.removalListener(this::onCacheRemoval)
.build();
metricsSystem.createIntegerGauge(
BesuMetricCategory.ETHEREUM,
"peer_count",
"The current number of peers connected",
activeConnections::size);
metricsSystem.createIntegerGauge(
BesuMetricCategory.ETHEREUM,
"peer_count_snap_server",
"The current number of peers connected that serve snap data",
() ->
(int) streamAvailablePeers().filter(EthPeerImmutableAttributes::isServingSnap).count());
metricsSystem.createIntegerGauge(
BesuMetricCategory.PEERS,
"pending_peer_requests_current",
"Number of peer requests currently pending because peers are busy",
pendingRequests::size);
metricsSystem.createIntegerGauge(
BesuMetricCategory.ETHEREUM,
"peer_limit",
"The maximum number of peers this node allows to connect",
() -> peerUpperBound);
metricsSystem.createIntegerGauge(
BesuMetricCategory.ETHEREUM,
"peer_count_incomplete",
"The current number of connections that have not yet completed the eth STATUS handshake",
() -> (int) incompleteConnections.size());
connectedPeersCounter =
metricsSystem.createCounter(
BesuMetricCategory.PEERS, "connected_total", "Total number of peers connected");
final LabelledSuppliedMetric peerClientLabelledGauge =
metricsSystem.createLabelledSuppliedGauge(
BesuMetricCategory.PEERS,
"peer_count_by_client",
"The number of clients connected by client",
"client");
for (final var clientName : PeerClientName.values()) {
peerClientLabelledGauge.labels(
() -> countConnectedPeersByClientName(clientName), clientName.getDisplayName());
}
}
private double countConnectedPeersByClientName(final PeerClientName clientName) {
return streamAllActiveConnections()
.map(PeerConnection::getPeerInfo)
.map(PeerInfo::getClientName)
.filter(clientName::equals)
.count();
}
public void registerNewConnection(
final PeerConnection newConnection, final List<PeerValidator> peerValidators) {
final Bytes id = newConnection.getPeer().getId();
synchronized (this) {
EthPeer ethPeer = activeConnections.get(id);
if (ethPeer == null) {
final Optional<EthPeer> peerInList =
incompleteConnections.asMap().values().stream()
.filter(p -> p.getId().equals(id))
.findFirst();
ethPeer =
peerInList.orElseGet(
() ->
new EthPeer(
newConnection,
this::ethPeerStatusExchanged,
peerValidators,
maxMessageSize,
clock,
permissioningProviders,
localNodeId));
}
incompleteConnections.put(newConnection, ethPeer);
}
}
@NotNull
private List<PeerConnection> getIncompleteConnections(final Bytes id) {
return incompleteConnections.asMap().keySet().stream()
.filter(nrc -> nrc.getPeer().getId().equals(id))
.collect(Collectors.toList());
}
public boolean registerDisconnect(final PeerConnection connection) {
final EthPeer peer = peer(connection);
return registerDisconnect(peer, connection);
}
private boolean registerDisconnect(final EthPeer peer, final PeerConnection connection) {
incompleteConnections.invalidate(connection);
boolean removed = false;
if (peer == null) {
LOG.atTrace()
.setMessage("attempt to remove null peer with connection {}")
.addArgument(connection)
.log();
return false;
}
if (peer.getConnection().equals(connection)) {
final Bytes id = peer.getId();
if (!peerHasIncompleteConnection(id)) {
removed = activeConnections.remove(id, peer);
disconnectCallbacks.forEach(callback -> callback.onDisconnect(peer));
peer.handleDisconnect();
abortPendingRequestsAssignedToDisconnectedPeers();
if (peer.getReputation().getScore() > USEFULL_PEER_SCORE_THRESHOLD) {
LOG.atDebug().setMessage("Disconnected USEFUL peer {}").addArgument(peer).log();
} else {
LOG.atDebug()
.setMessage("Disconnected EthPeer {}")
.addArgument(peer.getLoggableId())
.log();
}
}
}
reattemptPendingPeerRequests();
return removed;
}
private boolean peerHasIncompleteConnection(final Bytes id) {
return getIncompleteConnections(id).stream().anyMatch(conn -> !conn.isDisconnected());
}
private void abortPendingRequestsAssignedToDisconnectedPeers() {
synchronized (this) {
for (final PendingPeerRequest request : pendingRequests) {
if (request.getAssignedPeer().map(EthPeer::isDisconnected).orElse(false)) {
request.abort();
}
}
}
}
public EthPeer peer(final PeerConnection connection) {
final EthPeer ethPeer = incompleteConnections.getIfPresent(connection);
return ethPeer != null ? ethPeer : activeConnections.get(connection.getPeer().getId());
}
public PendingPeerRequest executePeerRequest(
final PeerRequest request, final long minimumBlockNumber, final Optional<EthPeer> peer) {
final long actualMinBlockNumber;
if (minimumBlockNumber > 0 && currentProtocolSpecSupplier.get().isPoS()) {
// if on PoS do not enforce a min block number, since the estimated chain height of the remote
// peer is not updated anymore.
actualMinBlockNumber = 0;
} else {
actualMinBlockNumber = minimumBlockNumber;
}
final PendingPeerRequest pendingPeerRequest =
new PendingPeerRequest(this, request, actualMinBlockNumber, peer);
synchronized (this) {
if (!pendingPeerRequest.attemptExecution()) {
pendingRequests.add(pendingPeerRequest);
}
}
return pendingPeerRequest;
}
public void dispatchMessage(
final EthPeer peer, final EthMessage ethMessage, final String protocolName) {
final Optional<RequestManager> maybeRequestManager = peer.dispatch(ethMessage, protocolName);
if (maybeRequestManager.isPresent() && peer.hasAvailableRequestCapacity()) {
reattemptPendingPeerRequests();
}
}
public void dispatchMessage(final EthPeer peer, final EthMessage ethMessage) {
dispatchMessage(peer, ethMessage, EthProtocol.NAME);
}
@VisibleForTesting
void reattemptPendingPeerRequests() {
synchronized (this) {
final Iterator<PendingPeerRequest> iterator = pendingRequests.iterator();
while (iterator.hasNext()
&& streamAvailablePeers()
.anyMatch(EthPeerImmutableAttributes::hasAvailableRequestCapacity)) {
final PendingPeerRequest request = iterator.next();
if (request.attemptExecution()) {
pendingRequests.remove(request);
}
}
}
}
public long subscribeConnect(final ConnectCallback callback) {
return connectCallbacks.subscribe(callback);
}
public void unsubscribeConnect(final long id) {
connectCallbacks.unsubscribe(id);
}
public void subscribeDisconnect(final DisconnectCallback callback) {
disconnectCallbacks.subscribe(callback);
}
public int peerCount() {
removeDisconnectedPeers();
return activeConnections.size();
}
public int getMaxPeers() {
return peerUpperBound;
}
public Stream<EthPeerImmutableAttributes> streamAllPeers() {
return activeConnections.values().stream().map(EthPeerImmutableAttributes::from);
}
private void removeDisconnectedPeers() {
activeConnections
.values()
.forEach(
ep -> {
if (ep.isDisconnected()) {
registerDisconnect(ep, ep.getConnection());
}
});
}
public Stream<EthPeerImmutableAttributes> streamAvailablePeers() {
return streamAllPeers().filter(peer -> !peer.isDisconnected());
}
public Stream<EthPeerImmutableAttributes> streamBestPeers() {
return streamAvailablePeers()
.filter(EthPeerImmutableAttributes::isFullyValidated)
.sorted(getBestPeerComparator().reversed());
}
public Optional<EthPeer> bestPeer() {
return streamAvailablePeers()
.max(getBestPeerComparator())
.map(EthPeerImmutableAttributes::ethPeer);
}
public Optional<EthPeer> bestPeerWithHeightEstimate() {
return bestPeerMatchingCriteria(p -> p.isFullyValidated() && p.hasEstimatedChainHeight());
}
public Optional<EthPeer> bestPeerMatchingCriteria(
final Predicate<EthPeerImmutableAttributes> matchesCriteria) {
return streamAvailablePeers()
.filter(matchesCriteria)
.max(getBestPeerComparator())
.map(EthPeerImmutableAttributes::ethPeer);
}
public void setBestPeerComparator(final Comparator<EthPeerImmutableAttributes> comparator) {
LOG.info("Updating the default best peer comparator");
bestPeerComparator = comparator;
}
public Comparator<EthPeerImmutableAttributes> getBestPeerComparator() {
return bestPeerComparator;
}
public void setRlpxAgent(final RlpxAgent rlpxAgent) {
this.rlpxAgent = rlpxAgent;
}
public Stream<PeerConnection> streamAllActiveConnections() {
return activeConnections.values().stream()
.map(EthPeer::getConnection)
.filter(c -> !c.isDisconnected());
}
public Stream<PeerConnection> streamAllConnections() {
return Stream.concat(
activeConnections.values().stream().map(EthPeer::getConnection),
incompleteConnections.asMap().keySet().stream())
.distinct()
.filter(c -> !c.isDisconnected());
}
public Optional<DisconnectReason> gatePeerConnection(final Peer peer, final boolean inbound) {
if (peer.getForkId().isPresent()) {
final ForkId forkId = peer.getForkId().get();
if (!forkIdManager.peerCheck(forkId)) {
LOG.atDebug()
.setMessage("Wrong fork id, not trying to connect to peer {}")
.addArgument(peer::getId)
.log();
return Optional.of(DisconnectReason.USELESS_PEER_BY_CHAIN_COMPARATOR);
}
}
final Bytes id = peer.getId();
if (alreadyConnectedOrConnecting(inbound, id)) {
LOG.atTrace()
.setMessage("not connecting to peer {} - already connected")
.addArgument(peer.getLoggableId())
.log();
return Optional.of(DisconnectReason.ALREADY_CONNECTED);
}
if (peerCount() < getMaxPeers() || needMoreSnapServers() || canExceedPeerLimits(id)) {
return Optional.empty();
} else {
return Optional.of(DisconnectReason.TOO_MANY_PEERS);
}
}
private boolean alreadyConnectedOrConnecting(final boolean inbound, final Bytes id) {
final EthPeer ethPeer = activeConnections.get(id);
if (ethPeer != null && !ethPeer.isDisconnected()) {
return true;
}
final List<PeerConnection> incompleteConnections = getIncompleteConnections(id);
return incompleteConnections.stream()
.anyMatch(c -> !c.isDisconnected() && (!inbound || (inbound && c.inboundInitiated())));
}
public void disconnectWorstUselessPeer() {
streamAvailablePeers()
.filter(p -> !canExceedPeerLimits(p.ethPeer().getId()))
.min(getBestPeerComparator())
.ifPresent(
peer -> {
LOG.atDebug()
.setMessage(
"disconnecting peer {}. Waiting for better peers. Current {} of max {}")
.addArgument(peer.ethPeer().getLoggableId())
.addArgument(this::peerCount)
.addArgument(this::getMaxPeers)
.log();
peer.ethPeer()
.disconnect(DisconnectMessage.DisconnectReason.USELESS_PEER_BY_CHAIN_COMPARATOR);
});
}
public void setChainHeadTracker(final ChainHeadTracker tracker) {
this.tracker = tracker;
}
public void setSnapServerChecker(final SnapServerChecker checker) {
this.snapServerChecker = checker;
}
public void snapServerPeersNeeded(final boolean b) {
this.snapServerPeersNeeded = b;
}
public void setTrailingPeerRequirementsSupplier(
final Supplier<TrailingPeerRequirements> tprSupplier) {
this.trailingPeerRequirementsSupplier = tprSupplier;
}
// Part of the PeerSelector interface, to be split apart later
@Override
public Optional<EthPeer> getPeer(final Predicate<EthPeerImmutableAttributes> filter) {
return streamAvailablePeers()
.filter(filter)
.filter(EthPeerImmutableAttributes::hasAvailableRequestCapacity)
.filter(EthPeerImmutableAttributes::isFullyValidated)
.max(getBestPeerComparator())
.map(EthPeerImmutableAttributes::ethPeer);
}
// Part of the PeerSelector interface, to be split apart later
@Override
public CompletableFuture<EthPeer> waitForPeer(
final Predicate<EthPeerImmutableAttributes> filter) {
final CompletableFuture<EthPeer> future = new CompletableFuture<>();
LOG.debug("Waiting for peer matching filter. {} peers currently connected.", peerCount());
// check for an existing peer matching the filter and use that if one is found
Optional<EthPeer> maybePeer = getPeer(filter);
if (maybePeer.isPresent()) {
LOG.debug("Found peer matching filter already connected!");
future.complete(maybePeer.get());
} else {
// no existing peer matches our filter. Subscribe to new connections until we find one
LOG.debug("Subscribing to new peer connections to wait until one matches filter");
final long subscriptionId =
subscribeConnect(
(peer) -> {
if (!future.isDone() && filter.test(EthPeerImmutableAttributes.from(peer))) {
LOG.debug("Found new peer matching filter!");
future.complete(peer);
} else {
LOG.debug("New peer does not match filter");
}
});
future.handle(
(peer, throwable) -> {
LOG.debug("Unsubscribing from new peer connections with ID {}", subscriptionId);
unsubscribeConnect(subscriptionId);
return null;
});
}
return future;
}
// Part of the PeerSelector interface, to be split apart later
@Override
public Optional<EthPeer> getPeerByPeerId(final PeerId peerId) {
return Optional.ofNullable(activeConnections.get(peerId.getId()));
}
@FunctionalInterface
public interface ConnectCallback {
void onPeerConnected(EthPeer newPeer);
}
@Override
public String toString() {
if (activeConnections.isEmpty()) {
return "0 EthPeers {}";
}
final String connectionsList =
activeConnections.values().stream()
.sorted()
.map(EthPeer::toString)
.collect(Collectors.joining(", \n"));
return activeConnections.size() + " EthPeers {\n" + connectionsList + '}';
}
private void ethPeerStatusExchanged(final EthPeer peer) {
// We have a connection to a peer that is on the right chain and is willing to connect to us.
// Find out what the EthPeer block height is and whether it can serve snap data (if we are doing
// snap sync)
LOG.debug("Peer {} status exchanged", peer);
assert tracker != null : "ChainHeadTracker must be set before EthPeers can be used";
CompletableFuture<BlockHeader> future = tracker.getBestHeaderFromPeer(peer);
future.whenComplete(
(peerHeadBlockHeader, error) -> {
if (peerHeadBlockHeader == null) {
LOG.debug(
"Failed to retrieve chain head info. Disconnecting {}... {}",
peer.getLoggableId(),
error);
peer.disconnect(
DisconnectMessage.DisconnectReason.USELESS_PEER_FAILED_TO_RETRIEVE_CHAIN_HEAD);
} else {
// we can check trailing peers now
final TrailingPeerRequirements trailingPeerRequirements =
trailingPeerRequirementsSupplier.get();
if (trailingPeerRequirements != null) {
if (peer.chainState().getEstimatedHeight()
< trailingPeerRequirements.getMinimumHeightToBeUpToDate()) {
if (!(getNumTrailingPeers(trailingPeerRequirements.getMinimumHeightToBeUpToDate())
< trailingPeerRequirements.getMaxTrailingPeers())) {
LOG.atTrace()
.setMessage(
"Adding trailing peer {} would exceed max trailing peers {}. Disconnecting...")
.addArgument(peer.getLoggableId())
.addArgument(trailingPeerRequirements.getMaxTrailingPeers())
.log();
peer.disconnect(
DisconnectMessage.DisconnectReason.USELESS_PEER_EXCEEDS_TRAILING_PEERS);
return;
}
}
}
peer.chainState().updateHeightEstimate(peerHeadBlockHeader.getNumber());
CompletableFuture<Void> isServingSnapFuture;
if (syncMode == SyncMode.SNAP) {
// even if we have finished the snap sync, we still want to know if the peer is a snap
// server
isServingSnapFuture =
CompletableFuture.runAsync(
() -> {
try {
checkIsSnapServer(peer, peerHeadBlockHeader);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
} else {
isServingSnapFuture = CompletableFuture.completedFuture(null);
}
isServingSnapFuture.thenRun(
() -> {
if (!peer.getConnection().isDisconnected() && addPeerToEthPeers(peer)) {
connectedPeersCounter.inc();
connectCallbacks.forEach(cb -> cb.onPeerConnected(peer));
}
});
}
});
}
private void checkIsSnapServer(final EthPeer peer, final BlockHeader peersHeadBlockHeader) {
if (peer.getAgreedCapabilities().contains(SnapProtocol.SNAP1)) {
if (snapServerChecker != null) {
// set that peer is a snap server for doing the test
peer.setIsServingSnap(true);
Boolean isServer;
try {
isServer = snapServerChecker.check(peer, peersHeadBlockHeader).get(6L, TimeUnit.SECONDS);
} catch (Exception e) {
LOG.atTrace()
.setMessage("Error checking if peer {} is a snap server. Setting to false.")
.addArgument(peer.getLoggableId())
.log();
peer.setIsServingSnap(false);
return;
}
peer.setIsServingSnap(isServer);
LOG.atTrace()
.setMessage("{}: peer {}")
.addArgument(isServer ? "Is a snap server" : "Is NOT a snap server")
.addArgument(peer.getLoggableId())
.log();
}
}
}
private int comparePeerPriorities(final EthPeer p1, final EthPeer p2) {
final PeerConnection a = p1.getConnection();
final PeerConnection b = p2.getConnection();
final boolean aCanExceedPeerLimits = canExceedPeerLimits(a.getPeer().getId());
final boolean bCanExceedPeerLimits = canExceedPeerLimits(b.getPeer().getId());
if (aCanExceedPeerLimits && !bCanExceedPeerLimits) {
return -1;
} else if (bCanExceedPeerLimits && !aCanExceedPeerLimits) {
return 1;
} else {
return randomPeerPriority
? compareByMaskedNodeId(a, b)
: compareConnectionInitiationTimes(a, b);
}
}
private boolean canExceedPeerLimits(final Bytes peerId) {
if (rlpxAgent == null) {
return false;
}
return rlpxAgent.canExceedConnectionLimits(peerId);
}
private int compareConnectionInitiationTimes(final PeerConnection a, final PeerConnection b) {
return Math.toIntExact(a.getInitiatedAt() - b.getInitiatedAt());
}
private int compareByMaskedNodeId(final PeerConnection a, final PeerConnection b) {
return a.getPeer().getId().xor(nodeIdMask).compareTo(b.getPeer().getId().xor(nodeIdMask));
}
private void enforceRemoteConnectionLimits() {
if (!shouldLimitRemoteConnections || peerCount() < maxRemotelyInitiatedConnections) {
// Nothing to do
return;
}
getActivePrioritizedPeers()
.filter(p -> p.getConnection().inboundInitiated())
.filter(p -> !canExceedPeerLimits(p.getId()))
.skip(maxRemotelyInitiatedConnections)
.forEach(
conn -> {
LOG.trace(
"Too many remotely initiated connections. Disconnect low-priority connection: {}, maxRemote={}",
conn,
maxRemotelyInitiatedConnections);
conn.disconnect(DisconnectMessage.DisconnectReason.TOO_MANY_PEERS);
});
}
private Stream<EthPeer> getActivePrioritizedPeers() {
return activeConnections.values().stream()
.filter(p -> !p.isDisconnected())
.sorted(this::comparePeerPriorities);
}
private void enforceConnectionLimits() {
if (peerCount() < peerUpperBound) {
// Nothing to do - we're under our limits
return;
}
getActivePrioritizedPeers()
.skip(peerUpperBound)
.map(EthPeer::getConnection)
.filter(c -> !canExceedPeerLimits(c.getPeer().getId()))
.forEach(
conn -> {
LOG.trace(
"Too many connections. Disconnect low-priority connection: {}, maxConnections={}",
conn,
peerUpperBound);
conn.disconnect(DisconnectMessage.DisconnectReason.TOO_MANY_PEERS);
});
}
private boolean inboundInitiatedConnectionLimitExceeded() {
return shouldLimitRemoteConnections
&& countUntrustedRemotelyInitiatedConnections() > maxRemotelyInitiatedConnections;
}
private long countUntrustedRemotelyInitiatedConnections() {
return activeConnections.values().stream()
.map(EthPeer::getConnection)
.filter(PeerConnection::inboundInitiated)
.filter(c -> !c.isDisconnected())
.filter(conn -> !canExceedPeerLimits(conn.getPeer().getId()))
.count();
}
// AERE D-434 (e540ae2394, GHSA-6r9q-wjp4-34gh): close pre-STATUS connections evicted from the
// bounded incomplete-connection cache so they cannot leak file descriptors or heap.
@VisibleForTesting
void onCacheRemoval(final RemovalNotification<PeerConnection, EthPeer> removalNotification) {
// Only react to evictions (size cap or expiry). Explicit invalidations (e.g. on a normal
// disconnect) already close the connection through their own path.
if (!removalNotification.wasEvicted()) {
return;
}
final PeerConnection evictedConnection = removalNotification.getKey();
final EthPeer peer = removalNotification.getValue();
if (evictedConnection == null || evictedConnection.isDisconnected()) {
return;
}
final boolean isCurrentConnectionOfPeer =
peer != null && evictedConnection.equals(peer.getConnection());
if (isCurrentConnectionOfPeer && peer.statusHasBeenReceived()) {
// The peer completed (or is completing) the eth STATUS handshake and is being promoted to an
// active connection; its incomplete-cache entry is expiring naturally. Leave the live
// connection alone - it is (or will be) tracked in activeConnections.
return;
}
// Either a superseded connection (the peer reconnected with a different connection), or a
// connection that completed the devp2p HELLO but never sent eth STATUS and has now been evicted
// (20s expiry, or pushed out of the bounded cache). Close the socket so evicted pre-STATUS
// connections cannot leak file descriptors or heap while remaining invisible to --max-peers.
DisconnectReason reason =
isCurrentConnectionOfPeer
? DisconnectMessage.DisconnectReason.TIMEOUT
: DisconnectMessage.DisconnectReason.ALREADY_CONNECTED;
LOG.atTrace()
.setMessage(
"Closing pre-STATUS connection {} evicted from incomplete-connection cache, reason {}")
.addArgument(evictedConnection::getPeerInfo)
.addArgument(reason)
.log();
evictedConnection.disconnect(reason);
}
@VisibleForTesting
int incompleteConnectionCount() {
return (int) incompleteConnections.size();
}
boolean addPeerToEthPeers(final EthPeer peer) {
// We have a connection to a peer that is on the right chain and is willing to connect to us.
// Figure out whether we want to add it to the active connections.
final PeerConnection connection = peer.getConnection();
if (activeConnections.containsValue(peer)) {
return false;
}
final Bytes id = peer.getId();
if (!randomPeerPriority) {
if (peerCount() >= peerUpperBound) {
final long numSnapServers = numberOfSnapServers();
final boolean inboundLimitExceeded = inboundInitiatedConnectionLimitExceeded();
// three reasons why we would disconnect an existing peer to accommodate the new peer
if (canExceedPeerLimits(id)
|| (snapServerPeersNeeded
&& numSnapServers < snapServerTargetNumber
&& peer.isServingSnap())
|| (inboundLimitExceeded && !peer.getConnection().inboundInitiated())) {
final boolean filterOutSnapServers =
snapServerPeersNeeded && (numSnapServers <= snapServerTargetNumber);
// find and disconnect the least useful peer we can disconnect
activeConnections.values().stream()
.filter(p -> !canExceedPeerLimits(p.getId()))
.map(EthPeerImmutableAttributes::from)
.filter(filterOutSnapServers ? p -> !p.isServingSnap() : p -> true)
.filter(inboundLimitExceeded ? p -> p.isInboundInitiated() : p -> true)
.min(MOST_USEFUL_PEER)
.ifPresentOrElse(
pe -> {
pe.ethPeer().disconnect(DisconnectMessage.DisconnectReason.TOO_MANY_PEERS);
LOG.atTrace()
.setMessage("Disconnecting peer {} to be replaced by prioritised peer {}")
.addArgument(pe.ethPeer().getLoggableId())
.addArgument(peer.getLoggableId())
.log();
},
() -> // disconnect the least useful peer
activeConnections.values().stream()
.filter(p -> !canExceedPeerLimits(p.getId()))
.map(EthPeerImmutableAttributes::from)
.min(MOST_USEFUL_PEER)
.ifPresent(
p -> {
p.ethPeer()
.disconnect(DisconnectMessage.DisconnectReason.TOO_MANY_PEERS);
LOG.atTrace()
.setMessage(
"Disconnecting peer {} to be replaced by prioritised peer {}")
.addArgument(p.ethPeer().getLoggableId())
.addArgument(peer.getLoggableId())
.log();
}));
} else {
LOG.atTrace()
.setMessage(
"Too many peers. Disconnect peer {} with connection: {}, max connections {}")
.addArgument(peer.getLoggableId())
.addArgument(connection)
.addArgument(peerUpperBound)
.log();
connection.disconnect(DisconnectMessage.DisconnectReason.TOO_MANY_PEERS);
return false;
}
}
final boolean added = (activeConnections.putIfAbsent(id, peer) == null);
if (added) {
LOG.atTrace()
.setMessage("Added peer {} with connection {} to activeConnections")
.addArgument(id)
.addArgument(connection)
.log();
} else {
LOG.atTrace()
.setMessage("Did not add peer {} with connection {} to activeConnections")
.addArgument(id)
.addArgument(connection)
.log();
}
return added;
} else {
// randomPeerPriority! Add the peer and if there are too many connections fix it
// TODO: random peer priority does not care yet about snap server peers -> check later
activeConnections.putIfAbsent(id, peer);
enforceRemoteConnectionLimits();
enforceConnectionLimits();
return activeConnections.containsKey(id);
}
}
private long getNumTrailingPeers(final long minimumHeightToBeUpToDate) {
return streamAvailablePeers()
.filter(p -> p.estimatedChainHeight() < minimumHeightToBeUpToDate)
.count();
}
private boolean needMoreSnapServers() {
return snapServerPeersNeeded && numberOfSnapServers() < snapServerTargetNumber;
}
private long numberOfSnapServers() {
return activeConnections.values().stream().filter(EthPeer::isServingSnap).count();
}
}

View File

@ -0,0 +1,870 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0008-aere-pq-aip22.patch.
*/
package org.hyperledger.besu.ethereum.eth.sync;
import static org.hyperledger.besu.util.FutureUtils.exceptionallyCompose;
import org.hyperledger.besu.consensus.merge.ForkchoiceEvent;
import org.hyperledger.besu.consensus.merge.UnverifiedForkchoiceListener;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.chain.BadBlockCause;
import org.hyperledger.besu.ethereum.chain.BadBlockManager;
import org.hyperledger.besu.ethereum.chain.BlockAddedEvent;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.chain.MutableBlockchain;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Difficulty;
import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader;
import org.hyperledger.besu.ethereum.eth.manager.EthContext;
import org.hyperledger.besu.ethereum.eth.manager.EthMessage;
import org.hyperledger.besu.ethereum.eth.manager.EthPeer;
import org.hyperledger.besu.ethereum.eth.manager.peertask.InvalidPeerTaskResponseException;
import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutorResponseCode;
import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutorResult;
import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetBodiesFromPeerTask;
import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetHeadersFromPeerTask;
import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetHeadersFromPeerTask.Direction;
import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages;
import org.hyperledger.besu.ethereum.eth.messages.NewBlockHashesMessage;
import org.hyperledger.besu.ethereum.eth.messages.NewBlockHashesMessage.NewBlockHash;
import org.hyperledger.besu.ethereum.eth.messages.NewBlockMessage;
import org.hyperledger.besu.ethereum.eth.sync.state.PendingBlocksManager;
import org.hyperledger.besu.ethereum.eth.sync.state.SyncState;
import org.hyperledger.besu.ethereum.eth.sync.tasks.PersistBlockTask;
import org.hyperledger.besu.ethereum.mainnet.BlockHeaderValidator;
import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason;
import org.hyperledger.besu.ethereum.rlp.RLPException;
import org.hyperledger.besu.plugin.data.AddedBlockContext.EventType;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BlockPropagationManager implements UnverifiedForkchoiceListener {
private static final Logger LOG = LoggerFactory.getLogger(BlockPropagationManager.class);
private static final long TIMEOUT = Duration.ofSeconds(60L).toMillis();
private final SynchronizerConfiguration config;
private final ProtocolSchedule protocolSchedule;
private final ProtocolContext protocolContext;
private final EthContext ethContext;
private final SyncState syncState;
private final MetricsSystem metricsSystem;
private final BlockBroadcaster blockBroadcaster;
private final AtomicBoolean started = new AtomicBoolean(false);
private final ProcessingBlocksManager processingBlocksManager;
private final PendingBlocksManager pendingBlocksManager;
private Optional<Long> onBlockAddedSId = Optional.empty();
private Optional<Long> newBlockSId;
private Optional<Long> newBlockHashesSId;
BlockPropagationManager(
final SynchronizerConfiguration config,
final ProtocolSchedule protocolSchedule,
final ProtocolContext protocolContext,
final EthContext ethContext,
final SyncState syncState,
final PendingBlocksManager pendingBlocksManager,
final MetricsSystem metricsSystem,
final BlockBroadcaster blockBroadcaster) {
this(
config,
protocolSchedule,
protocolContext,
ethContext,
syncState,
pendingBlocksManager,
metricsSystem,
blockBroadcaster,
new ProcessingBlocksManager());
}
BlockPropagationManager(
final SynchronizerConfiguration config,
final ProtocolSchedule protocolSchedule,
final ProtocolContext protocolContext,
final EthContext ethContext,
final SyncState syncState,
final PendingBlocksManager pendingBlocksManager,
final MetricsSystem metricsSystem,
final BlockBroadcaster blockBroadcaster,
final ProcessingBlocksManager processingBlocksManager) {
this.config = config;
this.protocolSchedule = protocolSchedule;
this.protocolContext = protocolContext;
this.ethContext = ethContext;
this.metricsSystem = metricsSystem;
this.blockBroadcaster = blockBroadcaster;
this.syncState = syncState;
this.pendingBlocksManager = pendingBlocksManager;
this.syncState.subscribeTTDReached(this::reactToTTDReachedEvent);
this.processingBlocksManager = processingBlocksManager;
}
public void start() {
if (started.compareAndSet(false, true)) {
setupListeners();
} else {
throw new IllegalStateException(
"Attempt to start an already started " + this.getClass().getSimpleName() + ".");
}
}
public void stop() {
if (started.get()) {
clearListeners();
started.set(false);
} else {
LOG.debug("Attempted to stop when we are not even running...");
}
}
public boolean isRunning() {
return started.get();
}
private void setupListeners() {
onBlockAddedSId =
Optional.of(protocolContext.getBlockchain().observeBlockAdded(this::onBlockAdded));
newBlockSId =
Optional.of(
ethContext
.getEthMessages()
.subscribe(EthProtocolMessages.NEW_BLOCK, this::handleNewBlockFromNetwork));
newBlockHashesSId =
Optional.of(
ethContext
.getEthMessages()
.subscribe(
EthProtocolMessages.NEW_BLOCK_HASHES, this::handleNewBlockHashesFromNetwork));
}
private void clearListeners() {
onBlockAddedSId.ifPresent(id -> protocolContext.getBlockchain().removeObserver(id));
newBlockSId.ifPresent(
id -> ethContext.getEthMessages().unsubscribe(id, EthProtocolMessages.NEW_BLOCK));
newBlockHashesSId.ifPresent(
id -> ethContext.getEthMessages().unsubscribe(id, EthProtocolMessages.NEW_BLOCK_HASHES));
onBlockAddedSId = Optional.empty();
newBlockSId = Optional.empty();
newBlockHashesSId = Optional.empty();
}
private void onBlockAdded(final BlockAddedEvent blockAddedEvent) {
// Check to see if any of our pending blocks are now ready for import
LOG.atTrace()
.setMessage("Block added event type {} for block {}. Current status {}")
.addArgument(blockAddedEvent::getEventType)
.addArgument(blockAddedEvent.getHeader()::toLogString)
.addArgument(this)
.log();
// If there is no children to process, maybe try non announced blocks
if (!maybeProcessPendingChildrenBlocks(blockAddedEvent.getHeader())) {
LOG.atTrace()
.setMessage("There are no pending blocks ready to import for block {}")
.addArgument(blockAddedEvent.getHeader()::toLogString)
.log();
maybeProcessNonAnnouncedBlocks(blockAddedEvent);
}
if (blockAddedEvent.getEventType().equals(EventType.HEAD_ADVANCED)) {
final long head = protocolContext.getBlockchain().getChainHeadBlockNumber();
final long cutoff = head + config.getBlockPropagationRange().lowerEndpoint();
pendingBlocksManager.purgeBlocksOlderThan(cutoff);
}
}
/**
* Process pending Children if any
*
* @param header the header of the block to process the children of
* @return true if block has any pending child
*/
private boolean maybeProcessPendingChildrenBlocks(final BlockHeader header) {
final List<Block> readyForImport;
synchronized (pendingBlocksManager) {
// Remove block from pendingBlocks list
pendingBlocksManager.deregisterPendingBlock(header);
// Import any pending blocks that are children of the newly added block
readyForImport = pendingBlocksManager.childrenOf(header.getHash());
}
if (!readyForImport.isEmpty()) {
LOG.atTrace()
.setMessage("Ready to import pending blocks found [{}] for block {}")
.addArgument(
() ->
readyForImport.stream().map(Block::toLogString).collect(Collectors.joining(", ")))
.addArgument(header::toLogString)
.log();
final Supplier<CompletableFuture<List<Block>>> importBlocksTask =
PersistBlockTask.forUnorderedBlocks(
protocolSchedule,
protocolContext,
ethContext,
readyForImport,
HeaderValidationMode.FULL,
metricsSystem);
ethContext
.getScheduler()
.scheduleSyncWorkerTask(importBlocksTask)
.whenComplete(
(r, t) -> {
if (r != null) {
LOG.info(
"Imported {} pending blocks: {}",
r.size(),
r.stream().map(b -> b.getHeader().getNumber()).collect(Collectors.toList()));
}
if (t != null) {
LOG.error("Error importing pending blocks", t);
}
});
}
return !readyForImport.isEmpty();
}
private void maybeProcessNonAnnouncedBlocks(final BlockAddedEvent newBlock) {
final long localHeadBlockNumber = protocolContext.getBlockchain().getChainHeadBlockNumber();
if (newBlock.getHeader().getNumber() > localHeadBlockNumber) {
pendingBlocksManager
.lowestAnnouncedBlock()
.map(ProcessableBlockHeader::getNumber)
.ifPresent(
minAnnouncedBlockNumber -> {
final long distance = minAnnouncedBlockNumber - localHeadBlockNumber;
LOG.trace(
"Found lowest announced block {} with distance {}",
minAnnouncedBlockNumber,
distance);
final long firstNonAnnouncedBlockNumber = newBlock.getHeader().getNumber() + 1;
if (distance < config.getBlockPropagationRange().upperEndpoint()
&& minAnnouncedBlockNumber > firstNonAnnouncedBlockNumber) {
if (processingBlocksManager.addNonAnnouncedBlocks(firstNonAnnouncedBlockNumber)) {
retrieveNonAnnouncedBlock(firstNonAnnouncedBlockNumber);
}
}
});
}
}
private void handleNewBlockFromNetwork(final EthMessage message) {
final Blockchain blockchain = protocolContext.getBlockchain();
final NewBlockMessage newBlockMessage = NewBlockMessage.readFrom(message.getData());
try {
final Block block = newBlockMessage.block(protocolSchedule);
LOG.atTrace()
.setMessage("New block from network {} from peer {}. Current status {}")
.addArgument(block::toLogString)
.addArgument(message::getPeer)
.addArgument(this)
.log();
final Difficulty totalDifficulty = newBlockMessage.totalDifficulty(protocolSchedule);
message.getPeer().chainState().updateForAnnouncedBlock(block.getHeader(), totalDifficulty);
// Return early if we don't care about this block
final long localChainHeight = protocolContext.getBlockchain().getChainHeadBlockNumber();
final long bestChainHeight = syncState.bestChainHeight(localChainHeight);
if (!shouldImportBlockAtHeight(
block.getHeader().getNumber(), localChainHeight, bestChainHeight)) {
LOG.atTrace()
.setMessage(
"Do not import new block from network {}, current chain heights are: local {}, best {}")
.addArgument(block::toLogString)
.addArgument(localChainHeight)
.addArgument(bestChainHeight)
.log();
return;
}
if (pendingBlocksManager.contains(block.getHash())) {
LOG.atTrace()
.setMessage("New block from network {} is already pending")
.addArgument(block::toLogString)
.log();
return;
}
if (blockchain.contains(block.getHash())) {
LOG.atTrace()
.setMessage("New block from network {} is already present")
.addArgument(block::toLogString)
.log();
return;
}
importOrSavePendingBlock(block, message.getPeer().nodeId());
} catch (final RLPException e) {
LOG.debug(
"Malformed NEW_BLOCK message received from peer (BREACH_OF_PROTOCOL), disconnecting: {}",
message.getPeer(),
e);
message.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED);
}
}
private void handleNewBlockHashesFromNetwork(final EthMessage message) {
final Blockchain blockchain = protocolContext.getBlockchain();
final NewBlockHashesMessage newBlockHashesMessage =
NewBlockHashesMessage.readFrom(message.getData());
try {
// Register announced blocks
final List<NewBlockHash> announcedBlocks =
Lists.newArrayList(newBlockHashesMessage.getNewHashes());
LOG.atTrace()
.setMessage("New block hashes from network {} from peer {}. Current status {}")
.addArgument(() -> toLogString(announcedBlocks))
.addArgument(message::getPeer)
.addArgument(this)
.log();
for (final NewBlockHash announcedBlock : announcedBlocks) {
message.getPeer().registerKnownBlock(announcedBlock.hash());
message.getPeer().registerHeight(announcedBlock.hash(), announcedBlock.number());
}
// Filter announced blocks for blocks we care to import
final long localChainHeight = protocolContext.getBlockchain().getChainHeadBlockNumber();
final long bestChainHeight = syncState.bestChainHeight(localChainHeight);
final List<NewBlockHash> relevantAnnouncements =
announcedBlocks.stream()
.filter(a -> shouldImportBlockAtHeight(a.number(), localChainHeight, bestChainHeight))
.collect(Collectors.toList());
// Filter for blocks we don't yet know about
final List<NewBlockHash> newBlocks = new ArrayList<>();
for (final NewBlockHash announcedBlock : relevantAnnouncements) {
if (pendingBlocksManager.contains(announcedBlock.hash())) {
LOG.trace("New block hash from network {} is already pending", announcedBlock);
continue;
}
if (processingBlocksManager.alreadyImporting(announcedBlock.hash())) {
LOG.trace("New block hash from network {} is already importing", announcedBlock);
continue;
}
if (blockchain.contains(announcedBlock.hash())) {
LOG.trace("New block hash from network {} was already imported", announcedBlock);
continue;
}
if (processingBlocksManager.addRequestedBlock(announcedBlock.hash())) {
newBlocks.add(announcedBlock);
} else {
LOG.trace("New block hash from network {} was already requested", announcedBlock);
}
}
// Process known blocks we care about
for (final NewBlockHash newBlock : newBlocks) {
processAnnouncedBlock(message.getPeer(), newBlock);
}
} catch (final RLPException e) {
LOG.debug(
"Malformed NEW_BLOCK_HASHES message received from peer (BREACH_OF_PROTOCOL), disconnecting: {}",
message.getPeer(),
e);
message.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED);
}
}
private CompletableFuture<Block> retrieveNonAnnouncedBlock(final long blockNumber) {
LOG.trace("Retrieve non announced block {} from peers", blockNumber);
return getBlockFromPeers(Optional.empty(), blockNumber, Optional.empty());
}
private CompletableFuture<Block> processAnnouncedBlock(
final EthPeer peer, final NewBlockHash blockHash) {
LOG.trace("Retrieve announced block by header {} from peers", blockHash);
return getBlockFromPeers(Optional.of(peer), blockHash.number(), Optional.of(blockHash.hash()));
}
private void requestParentBlock(final Block block) {
final BlockHeader blockHeader = block.getHeader();
if (processingBlocksManager.addRequestedBlock(blockHeader.getParentHash())) {
retrieveParentBlock(blockHeader);
} else {
LOG.debug("Parent block with hash {} is already requested", blockHeader.getParentHash());
}
}
private CompletableFuture<Block> retrieveParentBlock(final BlockHeader blockHeader) {
final long targetParentBlockNumber = blockHeader.getNumber() - 1L;
final Hash targetParentBlockHash = blockHeader.getParentHash();
LOG.info("Retrieving parent {} of block {}", targetParentBlockHash, blockHeader.toLogString());
return getBlockFromPeers(
Optional.empty(), targetParentBlockNumber, Optional.of(targetParentBlockHash));
}
private CompletableFuture<Block> getBlockFromPeers(
final Optional<EthPeer> preferredPeer,
final long blockNumber,
final Optional<Hash> maybeBlockHash) {
return repeatableGetBlockFromPeer(preferredPeer, blockNumber, maybeBlockHash)
.whenComplete(
(block, throwable) -> {
if (block != null) {
LOG.atDebug()
.setMessage("Successfully retrieved block {}")
.addArgument(block::toLogString)
.log();
processingBlocksManager.registerReceivedBlock(block);
} else {
if (throwable != null) {
LOG.warn(
"Failed to retrieve block "
+ logBlockNumberMaybeHash(blockNumber, maybeBlockHash),
throwable);
} else {
// this could happen if we give up at some point since we find that it make no
// sense to retry
LOG.atDebug()
.setMessage("Block {} not retrieved")
.addArgument(() -> logBlockNumberMaybeHash(blockNumber, maybeBlockHash))
.log();
}
processingBlocksManager.registerFailedGetBlock(blockNumber, maybeBlockHash);
}
});
}
private CompletableFuture<Block> repeatableGetBlockFromPeer(
final Optional<EthPeer> preferredPeer,
final long blockNumber,
final Optional<Hash> maybeBlockHash) {
return exceptionallyCompose(
scheduleGetBlockFromPeers(preferredPeer, blockNumber, maybeBlockHash),
handleGetBlockErrors(blockNumber, maybeBlockHash))
.thenCompose(r -> maybeRepeatGetBlock(blockNumber, maybeBlockHash));
}
private Function<Throwable, CompletionStage<Block>> handleGetBlockErrors(
final long blockNumber, final Optional<Hash> maybeBlockHash) {
return throwable -> {
LOG.atDebug()
.setMessage("Temporary failure retrieving block {} from peers with error {}")
.addArgument(() -> logBlockNumberMaybeHash(blockNumber, maybeBlockHash))
.addArgument(throwable)
.log();
return CompletableFuture.completedFuture(null);
};
}
private CompletableFuture<Block> maybeRepeatGetBlock(
final long blockNumber, final Optional<Hash> maybeBlockHash) {
final MutableBlockchain blockchain = protocolContext.getBlockchain();
final Optional<Block> maybeBlock =
maybeBlockHash
.map(hash -> blockchain.getBlockByHash(hash))
.orElseGet(() -> blockchain.getBlockByNumber(blockNumber));
// check if we got this block by other means
if (maybeBlock.isPresent()) {
final Block block = maybeBlock.get();
LOG.atDebug()
.setMessage("No need to retry to get block {} since it is already present")
.addArgument(block::toLogString)
.log();
return CompletableFuture.completedFuture(block);
}
final long localChainHeight = blockchain.getChainHeadBlockNumber();
final long bestChainHeight = syncState.bestChainHeight(localChainHeight);
if (!shouldImportBlockAtHeight(blockNumber, localChainHeight, bestChainHeight)) {
LOG.atDebug()
.setMessage("Not retrying to get block {} since we are too far from local chain head {}")
.addArgument(() -> logBlockNumberMaybeHash(blockNumber, maybeBlockHash))
.addArgument(blockchain.getChainHead()::toLogString)
.log();
return CompletableFuture.completedFuture(null);
}
LOG.atDebug()
.setMessage("Retrying to get block {}")
.addArgument(() -> logBlockNumberMaybeHash(blockNumber, maybeBlockHash))
.log();
return ethContext
.getScheduler()
.scheduleSyncWorkerTask(
() -> repeatableGetBlockFromPeer(Optional.empty(), blockNumber, maybeBlockHash));
}
private CompletableFuture<Block> scheduleGetBlockFromPeers(
final Optional<EthPeer> maybePreferredPeer,
final long blockNumber,
final Optional<Hash> maybeBlockHash) {
return ethContext
.getScheduler()
.scheduleServiceTask(() -> getBlockHeader(maybePreferredPeer, blockNumber, maybeBlockHash))
.thenApply((blockHeader) -> getBlock(maybePreferredPeer, blockHeader))
.thenCompose(
blockExecutorResult ->
importOrSavePendingBlock(
blockExecutorResult.result().get().getFirst(),
blockExecutorResult.ethPeers().getLast().nodeId()))
.orTimeout(TIMEOUT, TimeUnit.MILLISECONDS);
}
private CompletableFuture<BlockHeader> getBlockHeader(
final Optional<EthPeer> maybePreferredPeer,
final long blockNumber,
final Optional<Hash> maybeBlockHash) {
GetHeadersFromPeerTask headersFromPeerTask =
new GetHeadersFromPeerTask(
maybeBlockHash.orElse(null),
blockNumber,
1,
0,
Direction.FORWARD,
Math.max(1, ethContext.getEthPeers().peerCount()),
protocolSchedule);
if (maybePreferredPeer.isPresent()) {
PeerTaskExecutorResult<List<BlockHeader>> executorResult =
ethContext
.getPeerTaskExecutor()
.executeAgainstPeer(headersFromPeerTask, maybePreferredPeer.get());
if (executorResult.result().isPresent()
&& executorResult.responseCode() == PeerTaskExecutorResponseCode.SUCCESS) {
return CompletableFuture.completedFuture(executorResult.result().get().getFirst());
}
}
PeerTaskExecutorResult<List<BlockHeader>> executorResult =
ethContext.getPeerTaskExecutor().execute(headersFromPeerTask);
if (executorResult.result().isEmpty()
|| executorResult.responseCode() != PeerTaskExecutorResponseCode.SUCCESS) {
return CompletableFuture.failedFuture(new InvalidPeerTaskResponseException());
} else {
return CompletableFuture.completedFuture(executorResult.result().get().getFirst());
}
}
private PeerTaskExecutorResult<List<Block>> getBlock(
final Optional<EthPeer> maybePreferredPeer, final BlockHeader blockHeader) {
GetBodiesFromPeerTask bodiesTask =
new GetBodiesFromPeerTask(List.of(blockHeader), protocolSchedule);
if (maybePreferredPeer.isPresent()) {
PeerTaskExecutorResult<List<Block>> executorResult =
ethContext.getPeerTaskExecutor().executeAgainstPeer(bodiesTask, maybePreferredPeer.get());
if (executorResult.result().isPresent()
&& executorResult.responseCode() == PeerTaskExecutorResponseCode.SUCCESS) {
return executorResult;
}
}
PeerTaskExecutorResult<List<Block>> executorResult =
ethContext.getPeerTaskExecutor().execute(bodiesTask);
if (executorResult.result().isEmpty()
|| executorResult.responseCode() != PeerTaskExecutorResponseCode.SUCCESS) {
throw new RuntimeException(new InvalidPeerTaskResponseException());
} else {
return executorResult;
}
}
private void broadcastBlock(final Block block, final BlockHeader parent) {
final Difficulty totalDifficulty =
protocolContext
.getBlockchain()
.getTotalDifficultyByHash(parent.getHash())
.get()
.add(block.getHeader().getDifficulty());
blockBroadcaster.propagate(block, totalDifficulty);
}
@VisibleForTesting
CompletableFuture<Block> importOrSavePendingBlock(final Block block, final Bytes nodeId) {
// Synchronize to avoid race condition where block import event fires after the
// blockchain.contains() check and before the block is registered, causing onBlockAdded() to be
// invoked for the parent of this block before we are able to register it.
LOG.atTrace()
.setMessage("Import or save pending block {}")
.addArgument(block::toLogString)
.log();
if (!protocolContext.getBlockchain().contains(block.getHeader().getParentHash())) {
// Block isn't connected to local chain, save it to pending blocks collection
if (savePendingBlock(block, nodeId)) {
// if block is saved as pending, try to resolve it
maybeProcessPendingBlocks(block);
}
return CompletableFuture.completedFuture(block);
}
if (!processingBlocksManager.addImportingBlock(block.getHash())) {
LOG.atTrace()
.setMessage("We're already importing this block {}")
.addArgument(block::toLogString)
.log();
return CompletableFuture.completedFuture(block);
}
if (protocolContext.getBlockchain().contains(block.getHash())) {
LOG.atTrace()
.setMessage("We've already imported this block {}")
.addArgument(block::toLogString)
.log();
processingBlocksManager.registerBlockImportDone(block.getHash());
return CompletableFuture.completedFuture(block);
}
final BlockHeader parent =
protocolContext
.getBlockchain()
.getBlockHeader(block.getHeader().getParentHash())
.orElseThrow(
() ->
new IllegalArgumentException(
"Incapable of retrieving header from non-existent parent of "
+ block.toLogString()));
final ProtocolSpec protocolSpec = protocolSchedule.getByBlockHeader(block.getHeader());
final BlockHeaderValidator blockHeaderValidator = protocolSpec.getBlockHeaderValidator();
final BadBlockManager badBlockManager = protocolContext.getBadBlockManager();
return ethContext
.getScheduler()
.scheduleSyncWorkerTask(
() ->
validateAndProcessPendingBlock(
blockHeaderValidator, block, parent, badBlockManager));
}
/**
* Save the given block.
*
* @param block the block to track
* @param nodeId node that sent the block
* @return true if the block was added (was not previously present)
*/
private boolean savePendingBlock(final Block block, final Bytes nodeId) {
synchronized (pendingBlocksManager) {
if (pendingBlocksManager.registerPendingBlock(block, nodeId)) {
LOG.info(
"Saved announced block for future import {} - {} saved block(s)",
block.toLogString(),
pendingBlocksManager.size());
return true;
}
return false;
}
}
/**
* Try to request the lowest ancestor for the given pending block or process the descendants if
* the ancestor is already in the chain
*/
private void maybeProcessPendingBlocks(final Block block) {
// Try to get the lowest ancestor pending for this block, so we can import it
final Optional<Block> lowestPending = pendingBlocksManager.pendingAncestorBlockOf(block);
if (lowestPending.isPresent()) {
final Block lowestPendingBlock = lowestPending.get();
// If the parent of the lowest ancestor is not in the chain, request it.
if (!protocolContext
.getBlockchain()
.contains(lowestPendingBlock.getHeader().getParentHash())) {
requestParentBlock(lowestPendingBlock);
} else {
LOG.trace("Parent block is already in the chain");
// if the parent is already imported, process its children
maybeProcessPendingChildrenBlocks(lowestPendingBlock.getHeader());
}
}
}
private CompletableFuture<Block> validateAndProcessPendingBlock(
final BlockHeaderValidator blockHeaderValidator,
final Block block,
final BlockHeader parent,
final BadBlockManager badBlockManager) {
final HeaderValidationMode validationMode = HeaderValidationMode.FULL;
if (blockHeaderValidator.validateHeader(
block.getHeader(), parent, protocolContext, validationMode)) {
// AERE D-412 (2026-09-26), Rule C: a valid header is not, from AIP-22 stage 5, a proof that a
// quorum agreed the block (it carries only the proposer's single Falcon seal). A validator does
// not ADOPT a propagated block at the tip of its own consensus height without a post-quantum
// commit quorum for it; the gate is inert below H_hdr and for non-validators, and the sync
// downloader (a different path) is untouched, so this is fork choice, not validation.
if (!PropagatedBlockAdoptionGate.mayAdopt(block.getHeader(), parent)) {
processingBlocksManager.registerBlockImportDone(block.getHash());
LOG.info(
"AERE D-412: not adopting propagated block {} at the tip without a post-quantum commit "
+ "quorum; this node's own consensus decides this height",
block.toLogString());
return CompletableFuture.completedFuture(block);
}
ethContext.getScheduler().scheduleSyncWorkerTask(() -> broadcastBlock(block, parent));
return runImportTask(block);
} else {
processingBlocksManager.registerBlockImportDone(block.getHash());
final String description = String.format("Failed header validation (%s)", validationMode);
badBlockManager.addBadBlock(block, BadBlockCause.fromValidationFailure(description));
LOG.warn(
"Added to bad block manager for invalid header, failed to import announced block {}",
block.toLogString());
return CompletableFuture.completedFuture(block);
}
}
private CompletableFuture<Block> runImportTask(final Block block) {
final PersistBlockTask importTask =
PersistBlockTask.create(
protocolSchedule,
protocolContext,
ethContext,
block,
HeaderValidationMode.NONE,
metricsSystem);
return importTask
.run()
.whenComplete(
(result, throwable) -> {
processingBlocksManager.registerBlockImportDone(block.getHash());
if (throwable != null) {
LOG.warn("Failed to import announced block {}", block.toLogString());
}
});
}
// Only import blocks within a certain range of our head and sync target
private boolean shouldImportBlockAtHeight(
final long blockNumber, final long localHeight, final long bestChainHeight) {
final long distanceFromLocalHead = blockNumber - localHeight;
final long distanceFromBestPeer = blockNumber - bestChainHeight;
final Range<Long> importRange = config.getBlockPropagationRange();
return importRange.contains(distanceFromLocalHead)
&& importRange.contains(distanceFromBestPeer);
}
private String toLogString(final Collection<NewBlockHash> newBlockHashs) {
return newBlockHashs.stream()
.map(NewBlockHash::toString)
.collect(Collectors.joining(", ", "[", "]"));
}
private void reactToTTDReachedEvent(final boolean ttdReached) {
if (started.get() && ttdReached) {
LOG.info("Block propagation was running, then ttd reached");
} else if (!started.get()) {
start();
}
}
@Override
public String toString() {
return "BlockPropagationManager{"
+ processingBlocksManager
+ ", pendingBlocksManager="
+ pendingBlocksManager
+ '}';
}
private String logBlockNumberMaybeHash(
final long blockNumber, final Optional<Hash> maybeBlockHash) {
return blockNumber + maybeBlockHash.map(h -> " (" + h + ")").orElse("");
}
@Override
public void onNewUnverifiedForkchoice(final ForkchoiceEvent event) {
if (event.hasValidFinalizedBlockHash()) {
stop();
}
}
static class ProcessingBlocksManager {
private final Set<Hash> importingBlocks = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Set<Hash> requestedBlocks = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Set<Long> requestedNonAnnouncedBlocks =
Collections.newSetFromMap(new ConcurrentHashMap<>());
boolean addRequestedBlock(final Hash hash) {
return requestedBlocks.add(hash);
}
public boolean addNonAnnouncedBlocks(final long blockNumber) {
return requestedNonAnnouncedBlocks.add(blockNumber);
}
public boolean alreadyImporting(final Hash hash) {
return importingBlocks.contains(hash);
}
public synchronized void registerReceivedBlock(final Block block) {
requestedBlocks.remove(block.getHash());
requestedNonAnnouncedBlocks.remove(block.getHeader().getNumber());
}
public synchronized void registerFailedGetBlock(
final long blockNumber, final Optional<Hash> maybeBlockHash) {
requestedNonAnnouncedBlocks.remove(blockNumber);
maybeBlockHash.ifPresent(requestedBlocks::remove);
}
public boolean addImportingBlock(final Hash hash) {
return importingBlocks.add(hash);
}
public void registerBlockImportDone(final Hash hash) {
importingBlocks.remove(hash);
}
@Override
public synchronized String toString() {
return "ProcessingBlocksManager{"
+ "importingBlocks="
+ importingBlocks
+ ", requestedBlocks="
+ requestedBlocks
+ ", requestedNonAnnouncedBlocks="
+ requestedNonAnnouncedBlocks
+ '}';
}
}
}

View File

@ -0,0 +1,372 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-syncstate-in-sync-lock.patch.
*/
package org.hyperledger.besu.ethereum.eth.sync.state;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.chain.ChainHead;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.DefaultSyncStatus;
import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.core.Synchronizer.InSyncListener;
import org.hyperledger.besu.ethereum.eth.manager.ChainHeadEstimate;
import org.hyperledger.besu.ethereum.eth.manager.EthPeer;
import org.hyperledger.besu.ethereum.eth.manager.EthPeers;
import org.hyperledger.besu.ethereum.eth.sync.common.checkpoint.Checkpoint;
import org.hyperledger.besu.ethereum.eth.sync.worldstate.WorldStateDownloadStatus;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason;
import org.hyperledger.besu.plugin.data.SyncStatus;
import org.hyperledger.besu.plugin.services.BesuEvents.InitialSyncCompletionListener;
import org.hyperledger.besu.plugin.services.BesuEvents.SyncStatusListener;
import org.hyperledger.besu.plugin.services.BesuEvents.TTDReachedListener;
import org.hyperledger.besu.util.Subscribers;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class SyncState {
private final Blockchain blockchain;
private final EthPeers ethPeers;
// Ensures checkInSync() re-evaluation gives a consistent view of sync status. A
// standalone lock is used instead of the object monitor to prevent checkInSync()
// causing a deadlock while synchronized on the object monitor.
// (AERE D-426: upstream hyperledger/besu#11041, ported)
private final Object inSyncLock = new Object();
private final AtomicLong inSyncSubscriberId = new AtomicLong();
private final Map<Long, InSyncTracker> inSyncTrackers = new ConcurrentHashMap<>();
private final Subscribers<SyncStatusListener> syncStatusListeners = Subscribers.create();
private final Subscribers<TTDReachedListener> ttdReachedListeners = Subscribers.create();
private final Subscribers<InitialSyncCompletionListener> completionListenerSubscribers =
Subscribers.create();
private volatile long chainHeightListenerId;
private volatile Optional<SyncTarget> syncTarget = Optional.empty();
private Optional<WorldStateDownloadStatus> worldStateDownloadStatus = Optional.empty();
private Optional<Long> newPeerListenerId;
private Optional<Boolean> reachedTerminalDifficulty = Optional.empty();
private final Optional<Checkpoint> checkpoint;
private volatile boolean isInitialSyncPhaseDone;
private volatile boolean isResyncNeeded;
private Optional<Address> maybeAccountToRepair = Optional.empty();
public SyncState(final Blockchain blockchain, final EthPeers ethPeers) {
this(blockchain, ethPeers, false, Optional.empty());
}
public SyncState(
final Blockchain blockchain,
final EthPeers ethPeers,
final boolean hasInitialSyncPhase,
final Optional<Checkpoint> checkpoint) {
this.blockchain = blockchain;
this.ethPeers = ethPeers;
isInitialSyncPhaseDone = !hasInitialSyncPhase;
blockchain.observeBlockAdded(
event -> {
if (event.isNewCanonicalHead()) {
checkInSync();
}
});
// Add new peer listener to prevent permissioned PoA network stalling on start-up.
// https://github.com/hyperledger/besu/issues/528
newPeerListenerId =
Optional.of(
ethPeers.subscribeConnect(
newPeer -> {
if (newPeer.readyForRequests()) {
checkInSync();
}
}));
this.checkpoint = checkpoint;
}
/**
* Add a listener that will be notified when this node's sync status changes. A node is considered
* in-sync if the local chain height is no more than {@code SYNC_TOLERANCE} behind the highest
* estimated remote chain height.
*
* @param listener The callback to invoke when the sync status changes
* @return An {@code Unsubscriber} that can be used to stop listening for these events
*/
public long subscribeInSync(final InSyncListener listener) {
return subscribeInSync(listener, Synchronizer.DEFAULT_IN_SYNC_TOLERANCE);
}
/**
* Add a listener that will be notified when this node's sync status changes. A node is considered
* in-sync if the local chain height is no more than {@code syncTolerance} behind the highest
* estimated remote chain height.
*
* @param listener The callback to invoke when the sync status changes
* @param syncTolerance The tolerance used to determine whether this node is in-sync. A value of
* zero means that the node is considered in-sync only when the local chain height is greater
* than or equal to the best estimated remote chain height.
* @return An {@code Unsubscriber} that can be used to stop listening for these events
*/
public long subscribeInSync(final InSyncListener listener, final long syncTolerance) {
final InSyncTracker inSyncTracker = InSyncTracker.create(listener, syncTolerance);
final long id = inSyncSubscriberId.incrementAndGet();
inSyncTrackers.put(id, inSyncTracker);
return id;
}
public boolean unsubscribeInSync(final long subscriberId) {
return inSyncTrackers.remove(subscriberId) != null;
}
public long subscribeSyncStatus(final SyncStatusListener listener) {
return syncStatusListeners.subscribe(listener);
}
public long subscribeTTDReached(final TTDReachedListener listener) {
return ttdReachedListeners.subscribe(listener);
}
public long subscribeCompletionReached(final InitialSyncCompletionListener listener) {
return completionListenerSubscribers.subscribe(listener);
}
public boolean unsubscribeSyncStatus(final long listenerId) {
return syncStatusListeners.unsubscribe(listenerId);
}
public boolean unsubscribeTTDReached(final long listenerId) {
return ttdReachedListeners.unsubscribe(listenerId);
}
public boolean unsubscribeInitialConditionReached(final long listenerId) {
return completionListenerSubscribers.unsubscribe(listenerId);
}
public Optional<SyncStatus> syncStatus() {
return syncStatus(syncTarget);
}
public Optional<SyncTarget> syncTarget() {
return syncTarget;
}
public void setSyncTarget(final EthPeer peer, final BlockHeader commonAncestor) {
final SyncTarget syncTarget = new SyncTarget(peer, commonAncestor);
replaceSyncTarget(Optional.of(syncTarget));
}
public void setSyncProgress(
final long startingBlock, final long currentBlock, final long highestBlock) {
final SyncStatus status =
new DefaultSyncStatus(
startingBlock, currentBlock, highestBlock, Optional.empty(), Optional.empty());
syncStatusListeners.forEach(c -> c.onSyncStatusChanged(Optional.of(status)));
}
public void setWorldStateDownloadStatus(final WorldStateDownloadStatus worldStateDownloadStatus) {
this.worldStateDownloadStatus = Optional.ofNullable(worldStateDownloadStatus);
}
public boolean isInSync() {
return isInSync(Synchronizer.DEFAULT_IN_SYNC_TOLERANCE);
}
public boolean isInSync(final long syncTolerance) {
return isInSync(
getLocalChainHead(), getSyncTargetChainHead(), getBestPeerChainHead(), syncTolerance);
}
public void setReachedTerminalDifficulty(final boolean stoppedAtTerminalDifficulty) {
this.reachedTerminalDifficulty = Optional.of(stoppedAtTerminalDifficulty);
ttdReachedListeners.forEach(listener -> listener.onTTDReached(stoppedAtTerminalDifficulty));
}
public Optional<Boolean> hasReachedTerminalDifficulty() {
if (isInitialSyncPhaseDone) {
return reachedTerminalDifficulty;
}
return Optional.of(Boolean.FALSE);
}
private boolean isInSync(
final ChainHead localChain,
final Optional<ChainHeadEstimate> syncTargetChain,
final Optional<ChainHeadEstimate> bestPeerChain,
final long syncTolerance) {
return isInitialSyncPhaseDone
&& reachedTerminalDifficulty.orElse(true)
// Sync target may be temporarily empty while we switch sync targets during a sync, so
// check both the sync target and our best peer to determine if we're in sync or not
&& isInSync(localChain, syncTargetChain, syncTolerance)
&& isInSync(localChain, bestPeerChain, syncTolerance);
}
private boolean isInSync(
final ChainHead localChain,
final Optional<ChainHeadEstimate> remoteChain,
final long syncTolerance) {
return remoteChain
.map(remoteState -> InSyncTracker.isInSync(localChain, remoteState, syncTolerance))
.orElse(true);
}
private ChainHead getLocalChainHead() {
return blockchain.getChainHead();
}
private Optional<ChainHeadEstimate> getSyncTargetChainHead() {
return syncTarget.map(SyncTarget::peer).map(EthPeer::chainStateSnapshot);
}
public Optional<ChainHeadEstimate> getBestPeerChainHead() {
return ethPeers.bestPeerWithHeightEstimate().map(EthPeer::chainStateSnapshot);
}
public void disconnectSyncTarget(final DisconnectReason reason) {
syncTarget.ifPresent(syncTarget -> syncTarget.peer().disconnect(reason));
}
public void clearSyncTarget() {
replaceSyncTarget(Optional.empty());
}
private synchronized void replaceSyncTarget(final Optional<SyncTarget> newTarget) {
if (syncTarget.equals(newTarget)) {
// Nothing to do
return;
}
syncTarget.ifPresent(this::removeEstimatedHeightListener);
syncTarget = newTarget;
newTarget.ifPresent(this::addEstimatedHeightListener);
publishSyncStatus(newTarget);
checkInSync();
}
private void publishSyncStatus(final Optional<SyncTarget> newTarget) {
final Optional<SyncStatus> syncStatus = syncStatus(newTarget);
syncStatusListeners.forEach(c -> c.onSyncStatusChanged(syncStatus));
}
private Optional<SyncStatus> syncStatus(final Optional<SyncTarget> maybeTarget) {
return maybeTarget.map(
target -> {
final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber();
final long commonAncestor = target.commonAncestor().getNumber();
final long highestKnownBlock = bestChainHeight(chainHeadBlockNumber);
return new DefaultSyncStatus(
commonAncestor,
chainHeadBlockNumber,
highestKnownBlock,
worldStateDownloadStatus.flatMap(WorldStateDownloadStatus::getPulledStates),
worldStateDownloadStatus.flatMap(WorldStateDownloadStatus::getKnownStates));
});
}
private void removeEstimatedHeightListener(final SyncTarget target) {
target.removePeerChainEstimatedHeightListener(chainHeightListenerId);
}
private void addEstimatedHeightListener(final SyncTarget target) {
chainHeightListenerId =
target.addPeerChainEstimatedHeightListener(estimatedHeight -> checkInSync());
}
public long getLocalChainHeight() {
return blockchain.getChainHeadBlockNumber();
}
public long bestChainHeight() {
final long localChainHeight = blockchain.getChainHeadBlockNumber();
return bestChainHeight(localChainHeight);
}
public long bestChainHeight(final long localChainHeight) {
return Math.max(
localChainHeight,
ethPeers
.bestPeerWithHeightEstimate()
.map(p -> p.chainState().getEstimatedHeight())
.orElse(localChainHeight));
}
/** Evaluates whether this node is in sync and notifies any tracker whose verdict changed. */
private void checkInSync() {
synchronized (inSyncLock) {
final ChainHead localChain = getLocalChainHead();
final Optional<ChainHeadEstimate> syncTargetChain = getSyncTargetChainHead();
final Optional<ChainHeadEstimate> bestPeerChain = getBestPeerChainHead();
// Remove listener when we've found a peer.
newPeerListenerId.ifPresent(
listenerId -> {
ethPeers.unsubscribeConnect(listenerId);
newPeerListenerId = Optional.empty();
});
inSyncTrackers
.values()
.forEach(
(syncTracker) -> syncTracker.checkState(localChain, syncTargetChain, bestPeerChain));
}
}
public Optional<Checkpoint> getCheckpoint() {
return checkpoint;
}
public boolean isInitialSyncPhaseDone() {
return isInitialSyncPhaseDone;
}
public void markInitialSyncPhaseAsDone() {
isInitialSyncPhaseDone = true;
isResyncNeeded = false;
completionListenerSubscribers.forEach(InitialSyncCompletionListener::onInitialSyncCompleted);
}
public boolean isResyncNeeded() {
return isResyncNeeded;
}
public void markResyncNeeded() {
isResyncNeeded = true;
}
public Optional<Address> getAccountToRepair() {
return maybeAccountToRepair;
}
public void markAccountToRepair(final Optional<Address> address) {
maybeAccountToRepair = address;
}
public void markInitialSyncRestart() {
isInitialSyncPhaseDone = false;
completionListenerSubscribers.forEach(InitialSyncCompletionListener::onInitialSyncRestart);
}
}

View File

@ -0,0 +1,494 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.ethereum.eth.transactions;
import static org.hyperledger.besu.ethereum.core.kzg.CKZG4844Helper.CELL_PROOFS_PER_BLOB;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.ACCESS_LIST_ENTRY_SHALLOW_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.ACCESS_LIST_STORAGE_KEY_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.BLOBS_WITH_COMMITMENTS_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.BLOB_PROOF_BUNDLE_SIZE_V0;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.BLOB_PROOF_BUNDLE_SIZE_V1;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.CODE_DELEGATION_ENTRY_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.EIP1559_AND_EIP4844_SHALLOW_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.FRONTIER_AND_ACCESS_LIST_SHALLOW_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.KZG_PROOF_CONTAINER_SHALLOW_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.KZG_PROOF_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_ACCESS_LIST_SHALLOW_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_CHAIN_ID_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_CODE_DELEGATION_LIST_SHALLOW_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_SHALLOW_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.OPTIONAL_TO_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.PAYLOAD_SHALLOW_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.PENDING_TRANSACTION_SHALLOW_SIZE;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MemorySize.calculateListShallowSize;
import org.hyperledger.besu.datatypes.AccessListEntry;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
/**
* Tracks the additional metadata associated with transactions to enable prioritization for mining
* and deciding which transactions to drop when the transaction pool reaches its size limit.
*/
public abstract class PendingTransaction
implements org.hyperledger.besu.datatypes.PendingTransaction {
public static final Byte MAX_SCORE = Byte.MAX_VALUE;
private static final int NOT_INITIALIZED = -1;
private static final AtomicLong TRANSACTIONS_ADDED = new AtomicLong();
private final Transaction transaction;
private final long addedAt;
private final long sequence; // Allows prioritization based on order transactions are added
private volatile byte score;
private int memorySize = NOT_INITIALIZED;
private PendingTransaction(
final Transaction transaction, final byte score, final long addedAt, final long sequence) {
this.transaction = transaction;
this.addedAt = addedAt;
this.sequence = sequence;
this.score = score;
}
private PendingTransaction(final Transaction transaction, final byte score, final long addedAt) {
this(transaction, score, addedAt, TRANSACTIONS_ADDED.getAndIncrement());
}
public static PendingTransaction newPendingTransaction(
final Transaction transaction,
final boolean isLocal,
final boolean hasPriority,
final byte score) {
return newPendingTransaction(
transaction, isLocal, hasPriority, score, System.currentTimeMillis());
}
public static PendingTransaction newPendingTransaction(
final Transaction transaction,
final boolean isLocal,
final boolean hasPriority,
final byte score,
final long addedAt) {
if (isLocal) {
if (hasPriority) {
return new Local.Priority(transaction, score, addedAt);
}
return new Local(transaction, score, addedAt);
}
if (hasPriority) {
return new Remote.Priority(transaction, score, addedAt);
}
return new Remote(transaction, score, addedAt);
}
@Override
public Transaction getTransaction() {
return transaction;
}
public Wei getGasPrice() {
return transaction.getGasPrice().orElse(Wei.ZERO);
}
public long getSequence() {
return sequence;
}
public long getNonce() {
return transaction.getNonce();
}
public Address getSender() {
return transaction.getSender();
}
public Hash getHash() {
return transaction.getHash();
}
@Override
public long getAddedAt() {
return addedAt;
}
@Override
public int memorySize() {
if (memorySize == NOT_INITIALIZED) {
memorySize = computeMemorySize();
}
return memorySize;
}
public byte getScore() {
return score;
}
public void decrementScore() {
// use temp var to avoid non-atomic update of volatile var
final byte newScore = (byte) (score - 1);
// check to avoid underflow
if (newScore < score) {
score = newScore;
}
}
public abstract PendingTransaction detachedCopy();
private int computeMemorySize() {
return switch (transaction.getType()) {
case FRONTIER -> computeFrontierMemorySize();
case ACCESS_LIST -> computeAccessListMemorySize();
case EIP1559 -> computeEIP1559MemorySize();
case BLOB -> computeBlobMemorySize();
case DELEGATE_CODE -> computeDelegateCodeMemorySize();
case AERE_PQ -> computeAerePqMemorySize();
}
+ PENDING_TRANSACTION_SHALLOW_SIZE;
}
private int computeFrontierMemorySize() {
return FRONTIER_AND_ACCESS_LIST_SHALLOW_SIZE
+ computePayloadMemorySize()
+ computeToMemorySize()
+ computeChainIdMemorySize();
}
private int computeAccessListMemorySize() {
return FRONTIER_AND_ACCESS_LIST_SHALLOW_SIZE
+ computePayloadMemorySize()
+ computeToMemorySize()
+ computeChainIdMemorySize()
+ computeAccessListEntriesMemorySize();
}
private int computeEIP1559MemorySize() {
return EIP1559_AND_EIP4844_SHALLOW_SIZE
+ computePayloadMemorySize()
+ computeToMemorySize()
+ computeChainIdMemorySize()
+ computeAccessListEntriesMemorySize();
}
private int computeBlobMemorySize() {
return computeEIP1559MemorySize()
+ OPTIONAL_SHALLOW_SIZE // for the versionedHashes field
+ computeBlobWithCommitmentsMemorySize();
}
private int computeDelegateCodeMemorySize() {
return computeEIP1559MemorySize() + computeCodeDelegationListMemorySize();
}
// AIP-20: an EIP-1559 shape plus the post-quantum authorization (record + two Bytes wrappers + the
// raw key and signature bytes, up to 7,219 of them for ML-DSA-87).
private static final int AERE_PQ_AUTHORIZATION_SHALLOW_SIZE = 64;
private int computeAerePqMemorySize() {
return computeEIP1559MemorySize()
+ OPTIONAL_SHALLOW_SIZE
+ transaction
.getPqAuthorization()
.map(a -> AERE_PQ_AUTHORIZATION_SHALLOW_SIZE + a.authorizationByteCount())
.orElse(0);
}
private int computeBlobWithCommitmentsMemorySize() {
final int blobCount = transaction.getBlobCount();
return switch (transaction.getBlobsWithCommitments().get().getBlobType()) {
case KZG_PROOF ->
OPTIONAL_SHALLOW_SIZE
+ BLOBS_WITH_COMMITMENTS_SIZE
+ calculateListShallowSize(blobCount) // list of KZGProofBundle
+ BLOB_PROOF_BUNDLE_SIZE_V0 * blobCount;
case KZG_CELL_PROOFS ->
OPTIONAL_SHALLOW_SIZE
+ BLOBS_WITH_COMMITMENTS_SIZE
+ calculateListShallowSize(blobCount) // list of KZGProofBundle
+ (BLOB_PROOF_BUNDLE_SIZE_V1
+ KZG_PROOF_CONTAINER_SHALLOW_SIZE
+ KZG_PROOF_SIZE * CELL_PROOFS_PER_BLOB)
* blobCount;
};
}
private int computePayloadMemorySize() {
return !transaction.getPayload().isEmpty()
? PAYLOAD_SHALLOW_SIZE + transaction.getPayload().size()
: 0;
}
private int computeToMemorySize() {
if (transaction.getTo().isPresent()) {
return OPTIONAL_TO_SIZE;
}
return 0;
}
private int computeChainIdMemorySize() {
if (transaction.getChainId().isPresent()) {
return OPTIONAL_CHAIN_ID_SIZE;
}
return 0;
}
private int computeAccessListEntriesMemorySize() {
return transaction
.getAccessList()
.map(
al -> {
int totalSize = OPTIONAL_ACCESS_LIST_SHALLOW_SIZE;
totalSize += al.size() * ACCESS_LIST_ENTRY_SHALLOW_SIZE;
totalSize +=
al.stream().map(AccessListEntry::storageKeys).mapToInt(List::size).sum()
* ACCESS_LIST_STORAGE_KEY_SIZE;
return totalSize;
})
.orElse(0);
}
private int computeCodeDelegationListMemorySize() {
return transaction
.getCodeDelegationList()
.map(
cd -> {
int totalSize = OPTIONAL_CODE_DELEGATION_LIST_SHALLOW_SIZE;
totalSize += cd.size() * CODE_DELEGATION_ENTRY_SIZE;
return totalSize;
})
.orElse(0);
}
public static List<Transaction> toTransactionList(
final Collection<PendingTransaction> transactionsInfo) {
return transactionsInfo.stream().map(PendingTransaction::getTransaction).toList();
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PendingTransaction that = (PendingTransaction) o;
return sequence == that.sequence;
}
@Override
public int hashCode() {
return 31 * Long.hashCode(sequence);
}
@Override
public String toString() {
return "Hash="
+ transaction.getHash()
+ ", nonce="
+ transaction.getNonce()
+ ", sender="
+ transaction.getSender()
+ ", addedAt="
+ addedAt
+ ", sequence="
+ sequence
+ ", isLocal="
+ isReceivedFromLocalSource()
+ ", hasPriority="
+ hasPriority()
+ ", score="
+ score
+ '}';
}
@Override
public String toTraceLog() {
return "{sequence: "
+ sequence
+ ", addedAt: "
+ addedAt
+ ", isLocal="
+ isReceivedFromLocalSource()
+ ", hasPriority="
+ hasPriority()
+ ", score="
+ score
+ ", "
+ transaction.toTraceLog()
+ "}";
}
public static class Local extends PendingTransaction {
public Local(final Transaction transaction, final byte score, final long addedAt) {
super(transaction, score, addedAt);
}
public Local(final Transaction transaction) {
this(transaction, MAX_SCORE, System.currentTimeMillis());
}
private Local(final long sequence, final byte score, final Transaction transaction) {
super(transaction, score, System.currentTimeMillis(), sequence);
}
@Override
public PendingTransaction detachedCopy() {
return new Local(getSequence(), getScore(), getTransaction().detachedCopy());
}
@Override
public boolean isReceivedFromLocalSource() {
return true;
}
@Override
public boolean hasPriority() {
return false;
}
public static class Priority extends Local {
public Priority(final Transaction transaction) {
this(transaction, MAX_SCORE, System.currentTimeMillis());
}
public Priority(final Transaction transaction, final byte score, final long addedAt) {
super(transaction, score, addedAt);
}
public Priority(final long sequence, final byte score, final Transaction transaction) {
super(sequence, score, transaction);
}
@Override
public PendingTransaction detachedCopy() {
return new Priority(getSequence(), getScore(), getTransaction().detachedCopy());
}
@Override
public boolean hasPriority() {
return true;
}
}
}
public static class Remote extends PendingTransaction {
public Remote(final Transaction transaction, final byte score, final long addedAt) {
super(transaction, score, addedAt);
}
public Remote(final Transaction transaction) {
this(transaction, MAX_SCORE, System.currentTimeMillis());
}
private Remote(final long sequence, final byte score, final Transaction transaction) {
super(transaction, score, System.currentTimeMillis(), sequence);
}
@Override
public PendingTransaction detachedCopy() {
return new Remote(getSequence(), getScore(), getTransaction().detachedCopy());
}
@Override
public boolean isReceivedFromLocalSource() {
return false;
}
@Override
public boolean hasPriority() {
return false;
}
public static class Priority extends Remote {
public Priority(final Transaction transaction) {
this(transaction, MAX_SCORE, System.currentTimeMillis());
}
public Priority(final Transaction transaction, final byte score, final long addedAt) {
super(transaction, score, addedAt);
}
public Priority(final long sequence, final byte score, final Transaction transaction) {
super(sequence, score, transaction);
}
@Override
public PendingTransaction detachedCopy() {
return new Priority(getSequence(), getScore(), getTransaction().detachedCopy());
}
@Override
public boolean hasPriority() {
return true;
}
}
}
/**
* The memory size of an object is calculated using the PendingTransactionEstimatedMemorySizeTest
* look there for the details of the calculation and to adapt the code when any of the related
* class changes its structure.
*/
public interface MemorySize {
int FRONTIER_AND_ACCESS_LIST_SHALLOW_SIZE = 896;
int EIP1559_AND_EIP4844_SHALLOW_SIZE = 1008;
int OPTIONAL_TO_SIZE = 104;
int OPTIONAL_CHAIN_ID_SIZE = 80;
int PAYLOAD_SHALLOW_SIZE = 32;
int ACCESS_LIST_STORAGE_KEY_SIZE = 32;
int ACCESS_LIST_ENTRY_SHALLOW_SIZE = 160;
int OPTIONAL_ACCESS_LIST_SHALLOW_SIZE = 40;
int OPTIONAL_CODE_DELEGATION_LIST_SHALLOW_SIZE = 40;
int CODE_DELEGATION_ENTRY_SIZE = 512;
int OPTIONAL_SHALLOW_SIZE = 16;
int KZG_PROOF_CONTAINER_SHALLOW_SIZE = 24;
int KZG_PROOF_SIZE = 112;
int BLOBS_WITH_COMMITMENTS_SIZE = 48;
int BLOB_PROOF_BUNDLE_SIZE_V0 = 131536;
int BLOB_PROOF_BUNDLE_SIZE_V1 = 393576;
int PENDING_TRANSACTION_SHALLOW_SIZE = 40;
/**
* This calculation is for an immutable List
*
* @param length the length of the list
* @return the memory shallow size of the list and the contained array
*/
static int calculateListShallowSize(final int length) {
// list object shallow size is 24.
// array w/o elements size is 16.
// for every 2 objects in the array add 8
return 40 + ((length + 1) / 2) * 8;
}
}
}

View File

@ -0,0 +1,877 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-syncstate-in-sync-lock.patch.
*/
package org.hyperledger.besu.ethereum.eth.sync.state;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.chain.MutableBlockchain;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockDataGenerator;
import org.hyperledger.besu.ethereum.core.BlockDataGenerator.BlockOptions;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
import org.hyperledger.besu.ethereum.core.Difficulty;
import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider;
import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.core.Synchronizer.InSyncListener;
import org.hyperledger.besu.ethereum.core.TransactionReceipt;
import org.hyperledger.besu.ethereum.eth.manager.ChainHeadEstimate;
import org.hyperledger.besu.ethereum.eth.manager.ChainState;
import org.hyperledger.besu.ethereum.eth.manager.EthPeer;
import org.hyperledger.besu.ethereum.eth.manager.EthPeers;
import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManager;
import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManagerTestBuilder;
import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManagerTestUtil;
import org.hyperledger.besu.ethereum.eth.manager.RespondingEthPeer;
import org.hyperledger.besu.ethereum.eth.sync.common.checkpoint.Checkpoint;
import org.hyperledger.besu.ethereum.eth.sync.common.checkpoint.ImmutableCheckpoint;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.plugin.data.SyncStatus;
import org.hyperledger.besu.plugin.services.BesuEvents.InitialSyncCompletionListener;
import org.hyperledger.besu.plugin.services.BesuEvents.SyncStatusListener;
import org.hyperledger.besu.plugin.services.BesuEvents.TTDReachedListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class SyncStateTest {
private static final Difficulty standardDifficultyPerBlock = Difficulty.ONE;
private static final long OUR_CHAIN_HEAD_NUMBER = 3;
private static final Difficulty OUR_CHAIN_DIFFICULTY =
standardDifficultyPerBlock.multiply(OUR_CHAIN_HEAD_NUMBER);
private static final long TARGET_CHAIN_DELTA = 20;
private static final long TARGET_CHAIN_HEIGHT = OUR_CHAIN_HEAD_NUMBER + TARGET_CHAIN_DELTA;
private static final Difficulty TARGET_DIFFICULTY =
standardDifficultyPerBlock.multiply(TARGET_CHAIN_HEIGHT);
private final InSyncListener inSyncListener = mock(InSyncListener.class);
private final InSyncListener inSyncListenerExact = mock(InSyncListener.class);
private final SyncStatusListener syncStatusListener = mock(SyncStatusListener.class);
private final BlockDataGenerator gen = new BlockDataGenerator(1);
private final Block genesisBlock =
gen.genesisBlock(new BlockOptions().setDifficulty(Difficulty.ZERO));
private final MutableBlockchain blockchain =
InMemoryKeyValueStorageProvider.createInMemoryBlockchain(genesisBlock);
@Captor ArgumentCaptor<Optional<SyncStatus>> syncStatusCaptor;
private EthProtocolManager ethProtocolManager;
private EthPeers ethPeers;
private RespondingEthPeer syncTargetPeer;
private RespondingEthPeer otherPeer;
private SyncState syncState;
@BeforeEach
public void setUp() {
ethProtocolManager =
EthProtocolManagerTestBuilder.builder()
.setBlockchain(blockchain)
.setWorldStateArchive(mock(WorldStateArchive.class))
.build();
ethPeers = spy(ethProtocolManager.ethContext().getEthPeers());
syncTargetPeer = createPeer(TARGET_CHAIN_HEIGHT);
otherPeer = createPeer(0);
advanceLocalChain(OUR_CHAIN_HEAD_NUMBER);
syncState = new SyncState(blockchain, ethPeers);
syncState.subscribeInSync(inSyncListener);
syncState.subscribeInSync(inSyncListenerExact, 0);
syncState.subscribeSyncStatus(syncStatusListener);
}
@Test
public void isInSync_noPeers() {
otherPeer.disconnect(DisconnectReason.REQUESTED);
syncTargetPeer.disconnect(DisconnectReason.REQUESTED);
syncState.clearSyncTarget();
assertThat(syncState.isInSync()).isTrue();
}
@Test
public void isInSync_singlePeerWithWorseChainBetterHeight() {
updateChainState(
otherPeer.getEthPeer(), TARGET_CHAIN_HEIGHT, OUR_CHAIN_DIFFICULTY.subtract(1L));
final EthPeer peer = mockWorseChain(otherPeer.getEthPeer());
doReturn(Optional.of(peer)).when(ethPeers).bestPeerWithHeightEstimate();
assertThat(syncState.syncTarget()).isEmpty(); // Sanity check
assertThat(syncState.isInSync()).isTrue();
assertThat(syncState.isInSync(0)).isTrue();
}
@Test
public void isInSync_singlePeerWithWorseChainWorseHeight() {
updateChainState(
otherPeer.getEthPeer(), OUR_CHAIN_HEAD_NUMBER - 1L, OUR_CHAIN_DIFFICULTY.subtract(1L));
final EthPeer peer = mockWorseChain(otherPeer.getEthPeer());
doReturn(Optional.of(peer)).when(ethPeers).bestPeerWithHeightEstimate();
assertThat(syncState.syncTarget()).isEmpty(); // Sanity check
assertThat(syncState.isInSync()).isTrue();
assertThat(syncState.isInSync(0)).isTrue();
}
@Test
public void isInSync_singlePeerWithBetterChainWorseHeight() {
updateChainState(otherPeer.getEthPeer(), OUR_CHAIN_HEAD_NUMBER - 1L, TARGET_DIFFICULTY);
final EthPeer peer = mockBetterChain(otherPeer.getEthPeer());
doReturn(Optional.of(peer)).when(ethPeers).bestPeerWithHeightEstimate();
assertThat(syncState.syncTarget()).isEmpty(); // Sanity check
assertThat(syncState.isInSync()).isTrue();
assertThat(syncState.isInSync(0)).isTrue();
}
@Test
public void isInSync_singlePeerWithBetterChainBetterHeight() {
updateChainState(otherPeer.getEthPeer(), TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY);
final EthPeer peer = mockBetterChain(otherPeer.getEthPeer());
doReturn(Optional.of(peer)).when(ethPeers).bestPeerWithHeightEstimate();
assertThat(syncState.syncTarget()).isEmpty(); // Sanity check
assertThat(syncState.isInSync()).isFalse();
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA - 1)).isFalse();
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA)).isTrue();
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA + 1)).isTrue();
}
@Test
public void isInSync_syncTargetWithBetterHeight() {
otherPeer.disconnect(DisconnectReason.REQUESTED);
setupOutOfSyncState();
assertThat(syncState.syncTarget()).isPresent(); // Sanity check
assertThat(syncState.isInSync()).isFalse();
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA - 1)).isFalse();
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA)).isTrue();
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA + 1)).isTrue();
}
@Test
public void isInSync_syncTargetWithWorseHeight() {
otherPeer.disconnect(DisconnectReason.REQUESTED);
final long heightDifference = 20L;
advanceLocalChain(TARGET_CHAIN_HEIGHT + heightDifference);
setupOutOfSyncState();
assertThat(syncState.syncTarget()).isPresent(); // Sanity check
assertThat(syncState.isInSync()).isTrue();
assertThat(syncState.isInSync(0)).isTrue();
}
@Test
public void isInSync_outOfSyncWithTargetAndOutOfSyncWithBestPeer() {
setupOutOfSyncState();
updateChainState(otherPeer.getEthPeer(), TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY);
doReturn(Optional.of(otherPeer.getEthPeer())).when(ethPeers).bestPeerWithHeightEstimate();
assertThat(syncState.isInSync()).isFalse();
assertThat(syncState.isInSync(0)).isFalse();
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA - 1)).isFalse();
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA)).isTrue();
assertThat(syncState.isInSync(TARGET_CHAIN_DELTA + 1)).isTrue();
}
@Test
public void isInSync_inSyncWithTargetOutOfSyncWithBestPeer() {
setupOutOfSyncState();
advanceLocalChain(TARGET_CHAIN_HEIGHT);
final long heightDifference = 20L;
updateChainState(
otherPeer.getEthPeer(),
TARGET_CHAIN_HEIGHT + heightDifference,
TARGET_DIFFICULTY.add(heightDifference));
doReturn(Optional.of(otherPeer.getEthPeer())).when(ethPeers).bestPeerWithHeightEstimate();
assertThat(syncState.isInSync()).isFalse();
assertThat(syncState.isInSync(0)).isFalse();
assertThat(syncState.isInSync(heightDifference - 1)).isFalse();
assertThat(syncState.isInSync(heightDifference)).isTrue();
assertThat(syncState.isInSync(heightDifference + 1)).isTrue();
}
@Test
public void isInSync_inSyncWithTargetInSyncWithBestPeer() {
setupOutOfSyncState();
advanceLocalChain(TARGET_CHAIN_HEIGHT);
updateChainState(otherPeer.getEthPeer(), TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY);
doReturn(Optional.of(otherPeer.getEthPeer())).when(ethPeers).bestPeerWithHeightEstimate();
assertThat(syncState.isInSync()).isTrue();
assertThat(syncState.isInSync(0)).isTrue();
}
@Test
public void shouldSwitchToInSyncWhenNoBetterPeersAreAvailable() {
setupOutOfSyncState();
otherPeer.disconnect(DisconnectReason.REQUESTED);
syncTargetPeer.disconnect(DisconnectReason.REQUESTED);
syncState.clearSyncTarget();
verify(inSyncListener).onInSyncStatusChange(true);
verify(inSyncListenerExact).onInSyncStatusChange(true);
verifyNoMoreInteractions(inSyncListener);
verifyNoMoreInteractions(inSyncListenerExact);
}
@Test
public void shouldBecomeInSyncWhenOurBlockchainCatchesUp() {
setupOutOfSyncState();
// Update to just within the default sync threshold
advanceLocalChain(TARGET_CHAIN_HEIGHT - Synchronizer.DEFAULT_IN_SYNC_TOLERANCE);
// We should register as in-sync with default tolerance, out-of-sync with exact tolerance
assertThat(syncState.isInSync()).isTrue();
assertThat(syncState.isInSync(0)).isFalse();
verify(inSyncListener).onInSyncStatusChange(true);
verify(inSyncListenerExact, never()).onInSyncStatusChange(true);
// Advance one more block
advanceLocalChain(TARGET_CHAIN_HEIGHT - Synchronizer.DEFAULT_IN_SYNC_TOLERANCE + 1);
// We should register as in-sync with default tolerance, out-of-sync with exact tolerance
assertThat(syncState.isInSync()).isTrue();
assertThat(syncState.isInSync(0)).isFalse();
verifyNoMoreInteractions(inSyncListener);
verify(inSyncListenerExact, never()).onInSyncStatusChange(true);
// Catch all the way up
advanceLocalChain(TARGET_CHAIN_HEIGHT);
// We should register as in-sync
assertThat(syncState.isInSync()).isTrue();
assertThat(syncState.isInSync(0)).isTrue();
verifyNoMoreInteractions(inSyncListener);
verify(inSyncListenerExact).onInSyncStatusChange(true);
}
@Test
public void addInSyncListener_whileOutOfSync() {
setupOutOfSyncState();
// Add listener
InSyncListener newListener = mock(InSyncListener.class);
syncState.subscribeInSync(newListener);
verify(newListener, never()).onInSyncStatusChange(false);
verify(newListener, never()).onInSyncStatusChange(true);
// Catch all the way up
advanceLocalChain(TARGET_CHAIN_HEIGHT);
// Fall out of sync
updateChainState(
syncTargetPeer.getEthPeer(),
TARGET_CHAIN_HEIGHT + Synchronizer.DEFAULT_IN_SYNC_TOLERANCE + 1L,
TARGET_DIFFICULTY.add(10L));
final ArgumentCaptor<Boolean> inSyncEventCaptor = ArgumentCaptor.forClass(Boolean.class);
verify(newListener, times(3)).onInSyncStatusChange(inSyncEventCaptor.capture());
final List<Boolean> syncChanges = inSyncEventCaptor.getAllValues();
assertThat(syncChanges.get(0)).isEqualTo(false);
assertThat(syncChanges.get(1)).isEqualTo(true);
assertThat(syncChanges.get(2)).isEqualTo(false);
}
@Test
public void addInSyncListener_whileInSync() {
setupOutOfSyncState();
// Catch all the way up
advanceLocalChain(TARGET_CHAIN_HEIGHT);
// Add listener
InSyncListener newListener = mock(InSyncListener.class);
syncState.subscribeInSync(newListener);
verify(newListener, never()).onInSyncStatusChange(false);
verify(newListener, never()).onInSyncStatusChange(true);
// Fall out of sync
updateChainState(
syncTargetPeer.getEthPeer(),
TARGET_CHAIN_HEIGHT + Synchronizer.DEFAULT_IN_SYNC_TOLERANCE + 1L,
TARGET_DIFFICULTY.add(10L));
verify(newListener).onInSyncStatusChange(false);
verify(newListener, never()).onInSyncStatusChange(true);
// Catch up
advanceLocalChain(TARGET_CHAIN_HEIGHT + 1L);
verify(newListener).onInSyncStatusChange(false);
verify(newListener).onInSyncStatusChange(true);
}
@Test
public void removeInSyncListener_doesntReceiveSubsequentEvents() {
final long syncTolerance = Synchronizer.DEFAULT_IN_SYNC_TOLERANCE + 1L;
setupOutOfSyncState();
// Add listener
InSyncListener newListener = mock(InSyncListener.class);
final long subscriberId = syncState.subscribeInSync(newListener, syncTolerance);
verify(newListener, never()).onInSyncStatusChange(anyBoolean());
// Remove listener
syncState.unsubscribeInSync(subscriberId);
// Catch all the way up
advanceLocalChain(TARGET_CHAIN_HEIGHT);
// We should not register the in-sync event
verify(newListener, never()).onInSyncStatusChange(anyBoolean());
// Fall out of sync
updateChainState(
syncTargetPeer.getEthPeer(),
TARGET_CHAIN_HEIGHT + syncTolerance + 1L,
TARGET_DIFFICULTY.add(10L));
// We should not register the sync event
verify(newListener, never()).onInSyncStatusChange(anyBoolean());
// Other listeners should keep running
verify(inSyncListenerExact, times(2)).onInSyncStatusChange(false);
verify(inSyncListenerExact).onInSyncStatusChange(true);
}
@Test
public void removeInSyncListener_addAdditionalListenerBeforeRemoving() {
final long syncTolerance = Synchronizer.DEFAULT_IN_SYNC_TOLERANCE + 1L;
setupOutOfSyncState();
// Add listener
InSyncListener listenerToRemove = mock(InSyncListener.class);
InSyncListener otherListener = mock(InSyncListener.class);
final long subscriberId = syncState.subscribeInSync(listenerToRemove, syncTolerance);
syncState.subscribeInSync(otherListener, syncTolerance);
// Remove listener
syncState.unsubscribeInSync(subscriberId);
// Catch all the way up
advanceLocalChain(TARGET_CHAIN_HEIGHT);
// We should not register the in-sync event
verify(listenerToRemove, never()).onInSyncStatusChange(anyBoolean());
// Fall out of sync
updateChainState(
syncTargetPeer.getEthPeer(),
TARGET_CHAIN_HEIGHT + syncTolerance + 1L,
TARGET_DIFFICULTY.add(10L));
// We should not register the in-sync event
verify(listenerToRemove, never()).onInSyncStatusChange(anyBoolean());
final ArgumentCaptor<Boolean> inSyncEventCaptor = ArgumentCaptor.forClass(Boolean.class);
verify(otherListener, times(3)).onInSyncStatusChange(inSyncEventCaptor.capture());
final List<Boolean> syncChanges = inSyncEventCaptor.getAllValues();
assertThat(syncChanges.get(0)).isEqualTo(false);
assertThat(syncChanges.get(1)).isEqualTo(true);
assertThat(syncChanges.get(2)).isEqualTo(false);
// Other listeners should keep running
verify(inSyncListenerExact).onInSyncStatusChange(true);
verify(inSyncListenerExact, times(2)).onInSyncStatusChange(false);
}
@Test
public void removeInSyncListener_addAdditionalListenerAfterRemoving() {
final long syncTolerance = Synchronizer.DEFAULT_IN_SYNC_TOLERANCE + 1L;
setupOutOfSyncState();
// Add listener
InSyncListener listenerToRemove = mock(InSyncListener.class);
InSyncListener otherListener = mock(InSyncListener.class);
final long subscriberId = syncState.subscribeInSync(listenerToRemove, syncTolerance);
// Remove listener
syncState.unsubscribeInSync(subscriberId);
// Add new listener
syncState.subscribeInSync(otherListener, syncTolerance);
// Catch all the way up
advanceLocalChain(TARGET_CHAIN_HEIGHT);
// We should not register the sync event
verify(listenerToRemove, never()).onInSyncStatusChange(anyBoolean());
// Fall out of sync
updateChainState(
syncTargetPeer.getEthPeer(),
TARGET_CHAIN_HEIGHT + syncTolerance + 1L,
TARGET_DIFFICULTY.add(10L));
// We should not register the sync event
verify(listenerToRemove, never()).onInSyncStatusChange(anyBoolean());
final ArgumentCaptor<Boolean> inSyncEventCaptor = ArgumentCaptor.forClass(Boolean.class);
verify(otherListener, times(3)).onInSyncStatusChange(inSyncEventCaptor.capture());
final List<Boolean> syncChanges = inSyncEventCaptor.getAllValues();
assertThat(syncChanges.get(0)).isEqualTo(false);
assertThat(syncChanges.get(1)).isEqualTo(true);
assertThat(syncChanges.get(2)).isEqualTo(false);
// Other listeners should keep running
verify(inSyncListenerExact, times(2)).onInSyncStatusChange(false);
verify(inSyncListenerExact).onInSyncStatusChange(true);
}
@Test
public void syncStatusListener_receivesEventWhenSyncTargetSet() {
syncState.setSyncTarget(syncTargetPeer.getEthPeer(), blockchain.getBlockHeader(3L).get());
verify(syncStatusListener).onSyncStatusChanged(syncStatusCaptor.capture());
assertThat(syncStatusCaptor.getAllValues()).hasSize(1);
final Optional<SyncStatus> syncStatus = syncStatusCaptor.getValue();
assertThat(syncStatus).isPresent();
assertThat(syncStatus.get().getStartingBlock()).isEqualTo(3L);
assertThat(syncStatus.get().getCurrentBlock()).isEqualTo(OUR_CHAIN_HEAD_NUMBER);
assertThat(syncStatus.get().getHighestBlock()).isEqualTo(TARGET_CHAIN_HEIGHT);
}
@Test
public void syncStatusListener_receivesEventWhenSyncTargetCleared() {
syncState.setSyncTarget(syncTargetPeer.getEthPeer(), blockchain.getBlockHeader(3L).get());
syncState.clearSyncTarget();
verify(syncStatusListener, times(2)).onSyncStatusChanged(syncStatusCaptor.capture());
List<Optional<SyncStatus>> events = syncStatusCaptor.getAllValues();
assertThat(events).hasSize(2);
// Check first value
final Optional<SyncStatus> syncingEvent = events.get(0);
assertThat(syncingEvent).isPresent();
assertThat(syncingEvent.get().getStartingBlock()).isEqualTo(3L);
assertThat(syncingEvent.get().getCurrentBlock()).isEqualTo(OUR_CHAIN_HEAD_NUMBER);
assertThat(syncingEvent.get().getHighestBlock()).isEqualTo(TARGET_CHAIN_HEIGHT);
// Check second value
final Optional<SyncStatus> clearedEvent = events.get(1);
assertThat(clearedEvent).isEmpty();
}
@Test
public void syncStatusListener_ignoreNoopChangesToSyncTarget() {
syncState.clearSyncTarget();
syncState.setSyncTarget(syncTargetPeer.getEthPeer(), blockchain.getBlockHeader(3L).get());
syncState.setSyncTarget(syncTargetPeer.getEthPeer(), blockchain.getBlockHeader(3L).get());
syncState.clearSyncTarget();
syncState.clearSyncTarget();
verify(syncStatusListener, times(2)).onSyncStatusChanged(syncStatusCaptor.capture());
List<Optional<SyncStatus>> events = syncStatusCaptor.getAllValues();
assertThat(events).hasSize(2);
// Check first value
final Optional<SyncStatus> syncingEvent = events.get(0);
assertThat(syncingEvent).isPresent();
assertThat(syncingEvent.get().getStartingBlock()).isEqualTo(3L);
assertThat(syncingEvent.get().getCurrentBlock()).isEqualTo(OUR_CHAIN_HEAD_NUMBER);
assertThat(syncingEvent.get().getHighestBlock()).isEqualTo(TARGET_CHAIN_HEIGHT);
// Check second value
final Optional<SyncStatus> clearedEvent = events.get(1);
assertThat(clearedEvent).isEmpty();
}
private RespondingEthPeer createPeer(final long blockHeight) {
return EthProtocolManagerTestUtil.createPeer(ethProtocolManager, blockHeight);
}
private EthPeer mockWorseChain(final EthPeer peer) {
return mockChainIsBetterThan(peer, false);
}
private EthPeer mockBetterChain(final EthPeer peer) {
return mockChainIsBetterThan(peer, true);
}
private EthPeer mockChainIsBetterThan(final EthPeer peer, final boolean isBetter) {
final ChainState chainState = spy(peer.chainState());
final ChainHeadEstimate chainStateSnapshot = spy(peer.chainStateSnapshot());
lenient().doReturn(isBetter).when(chainState).chainIsBetterThan(any());
lenient().doReturn(isBetter).when(chainStateSnapshot).chainIsBetterThan(any());
final EthPeer mockedPeer = spy(peer);
lenient().doReturn(chainStateSnapshot).when(chainState).getSnapshot();
lenient().doReturn(chainStateSnapshot).when(mockedPeer).chainStateSnapshot();
lenient().doReturn(chainState).when(mockedPeer).chainState();
return mockedPeer;
}
private void setupOutOfSyncState() {
syncState.setSyncTarget(syncTargetPeer.getEthPeer(), blockchain.getGenesisBlock().getHeader());
verify(inSyncListener).onInSyncStatusChange(false);
verify(inSyncListenerExact).onInSyncStatusChange(false);
}
@Test
public void inSyncCheckDrivenByABlockImportDoesNotTakeTheSyncStateMonitor() {
// AERE D-426: upstream hyperledger/besu#11041, ported (unnamed lambda parameter spelled out for Java 21)
final List<Boolean> heldMonitorDuringCallback = new ArrayList<>();
syncState.subscribeInSync(
ignored -> heldMonitorDuringCallback.add(Thread.holdsLock(syncState)),
Synchronizer.DEFAULT_IN_SYNC_TOLERANCE);
// Fires the block-added observer and therefore calls checkInSync() on this thread.
advanceLocalChain(blockchain.getChainHeadBlockNumber() + 1);
assertThat(heldMonitorDuringCallback)
.withFailMessage("a block import called checkInSync() while holding the sync state monitor")
.isNotEmpty()
.containsOnly(false);
}
/**
* AERE D-426: the chain that froze validator 2 of testnet 28001 for 3 h 18 min on 2026-09-28. The chain downloader
* sets a sync target (replaceSyncTarget holds this monitor) and publishes the sync status; the BFT mining
* coordinator's listener stops the coordinator and waits for the BFT thread, which is importing a block, and the
* block-added observer calls checkInSync(). With checkInSync() on the monitor the import cannot finish while the
* listener waits, so each side waits for the other.
*/
@Test
public void syncStatusListenerWaitingOnABlockImportDoesNotDeadlockTheImport() throws Exception {
final CountDownLatch inCallback = new CountDownLatch(1);
final CountDownLatch blockAppended = new CountDownLatch(1);
final AtomicBoolean importFinishedWhileListenerWaited = new AtomicBoolean();
final AtomicReference<Throwable> importFailure = new AtomicReference<>();
syncState.subscribeSyncStatus(
ignored -> {
if (inCallback.getCount() == 0) {
return;
}
inCallback.countDown();
try {
// stands for BftMiningCoordinator.stop() -> BftProcessor.awaitStop()
importFinishedWhileListenerWaited.set(blockAppended.await(5, TimeUnit.SECONDS));
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
});
final long headBefore = blockchain.getChainHeadBlockNumber();
final Thread importer =
new Thread(
() -> {
try {
if (!inCallback.await(5, TimeUnit.SECONDS)) {
return;
}
// stands for QbftRound.importSealedBlock on the BFT thread
advanceLocalChain(headBefore + 1);
blockAppended.countDown();
} catch (final Throwable t) {
importFailure.set(t);
}
},
"aere-d426-importer");
importer.start();
// publishes on this thread, as PipelineChainDownloader.startDownloadForSyncTarget does
syncState.setSyncTarget(syncTargetPeer.getEthPeer(), blockchain.getGenesisBlock().getHeader());
importer.join(TimeUnit.SECONDS.toMillis(30));
assertThat(importer.isAlive()).withFailMessage("the importer thread never finished").isFalse();
assertThat(importFailure.get()).withFailMessage("the import itself failed: %s", importFailure.get()).isNull();
assertThat(inCallback.getCount()).withFailMessage("the sync status was never published").isZero();
assertThat(blockchain.getChainHeadBlockNumber()).isEqualTo(headBefore + 1);
assertThat(importFinishedWhileListenerWaited)
.withFailMessage(
"a block import could not finish while a sync-status listener waited for it: "
+ "checkInSync() is taking the monitor that replaceSyncTarget() holds (D-426)")
.isTrue();
}
private void advanceLocalChain(final long newChainHeight) {
while (blockchain.getChainHeadBlockNumber() < newChainHeight) {
final BlockHeader parent = blockchain.getChainHeadHeader();
final Block block =
gen.block(
BlockOptions.create()
.setDifficulty(standardDifficultyPerBlock)
.setParentHash(parent.getHash())
.setBlockNumber(parent.getNumber() + 1L)
.transactionCount(0));
final List<TransactionReceipt> receipts = gen.receipts(block);
blockchain.appendBlock(block, receipts);
}
}
/**
* Updates the chain state, such that the peer will end up with an estimated height of {@code
* blockHeight} and an estimated total difficulty of {@code totalDifficulty}
*
* @param peer The peer whose chain should be updated
* @param blockHeight The target estimated block height
* @param totalDifficulty The total difficulty
*/
private void updateChainState(
final EthPeer peer, final long blockHeight, final Difficulty totalDifficulty) {
// Chain state is updated based on the parent of the announced block
// So, increment block number by 1 and set block difficulty to zero
// in order to update to the values we want
final BlockHeader header =
new BlockHeaderTestFixture()
.number(blockHeight + 1L)
.difficulty(Difficulty.ZERO)
.buildHeader();
peer.chainState().updateForAnnouncedBlock(header, totalDifficulty);
// Sanity check this logic still holds
assertThat(peer.chainState().getEstimatedHeight()).isEqualTo(blockHeight);
assertThat(peer.chainState().getEstimatedTotalDifficulty()).isEqualTo(totalDifficulty);
}
@Test
public void shouldSubscribeAndUnsubscribeTTDReachedListener() {
TTDReachedListener listener = mock(TTDReachedListener.class);
long listenerId = syncState.subscribeTTDReached(listener);
assertThat(listenerId).isGreaterThanOrEqualTo(0);
boolean unsubscribed = syncState.unsubscribeTTDReached(listenerId);
assertThat(unsubscribed).isTrue();
// Unsubscribing again should return false
boolean unsubscribedAgain = syncState.unsubscribeTTDReached(listenerId);
assertThat(unsubscribedAgain).isFalse();
}
@Test
public void shouldNotifyTTDReachedListeners() {
TTDReachedListener listener = mock(TTDReachedListener.class);
syncState.subscribeTTDReached(listener);
syncState.setReachedTerminalDifficulty(true);
verify(listener).onTTDReached(true);
}
@Test
public void shouldNotifyMultipleTTDReachedListeners() {
TTDReachedListener listener1 = mock(TTDReachedListener.class);
TTDReachedListener listener2 = mock(TTDReachedListener.class);
syncState.subscribeTTDReached(listener1);
syncState.subscribeTTDReached(listener2);
syncState.setReachedTerminalDifficulty(false);
verify(listener1).onTTDReached(false);
verify(listener2).onTTDReached(false);
}
@Test
public void shouldSubscribeAndUnsubscribeCompletionListener() {
InitialSyncCompletionListener listener = mock(InitialSyncCompletionListener.class);
long listenerId = syncState.subscribeCompletionReached(listener);
assertThat(listenerId).isGreaterThanOrEqualTo(0);
boolean unsubscribed = syncState.unsubscribeInitialConditionReached(listenerId);
assertThat(unsubscribed).isTrue();
// Unsubscribing again should return false
boolean unsubscribedAgain = syncState.unsubscribeInitialConditionReached(listenerId);
assertThat(unsubscribedAgain).isFalse();
}
@Test
public void shouldNotifyCompletionListenersWhenSyncCompleted() {
SyncState syncStateWithInitialPhase =
new SyncState(blockchain, ethPeers, true, Optional.empty());
InitialSyncCompletionListener listener = mock(InitialSyncCompletionListener.class);
syncStateWithInitialPhase.subscribeCompletionReached(listener);
syncStateWithInitialPhase.markInitialSyncPhaseAsDone();
verify(listener).onInitialSyncCompleted();
}
@Test
public void shouldNotifyCompletionListenersOnRestart() {
SyncState syncStateWithInitialPhase =
new SyncState(blockchain, ethPeers, true, Optional.empty());
InitialSyncCompletionListener listener = mock(InitialSyncCompletionListener.class);
syncStateWithInitialPhase.subscribeCompletionReached(listener);
syncStateWithInitialPhase.markInitialSyncRestart();
verify(listener).onInitialSyncRestart();
}
@Test
public void shouldHandleCheckpointConstruction() {
Checkpoint checkpoint =
ImmutableCheckpoint.builder()
.blockNumber(100L)
.blockHash(Hash.ZERO)
.totalDifficulty(Difficulty.of(1000L))
.build();
SyncState syncStateWithCheckpoint =
new SyncState(blockchain, ethPeers, false, Optional.of(checkpoint));
assertThat(syncStateWithCheckpoint.getCheckpoint()).isPresent();
assertThat(syncStateWithCheckpoint.getCheckpoint().get()).isEqualTo(checkpoint);
}
@Test
public void shouldReturnEmptyCheckpointWhenNotSet() {
assertThat(syncState.getCheckpoint()).isEmpty();
}
@Test
public void shouldTrackInitialSyncPhaseState() {
SyncState syncStateWithPhase = new SyncState(blockchain, ethPeers, true, Optional.empty());
assertThat(syncStateWithPhase.isInitialSyncPhaseDone()).isFalse();
syncStateWithPhase.markInitialSyncPhaseAsDone();
assertThat(syncStateWithPhase.isInitialSyncPhaseDone()).isTrue();
syncStateWithPhase.markInitialSyncRestart();
assertThat(syncStateWithPhase.isInitialSyncPhaseDone()).isFalse();
}
@Test
public void shouldTrackResyncNeeded() {
assertThat(syncState.isResyncNeeded()).isFalse();
syncState.markResyncNeeded();
assertThat(syncState.isResyncNeeded()).isTrue();
syncState.markInitialSyncPhaseAsDone();
assertThat(syncState.isResyncNeeded()).isFalse();
}
@Test
public void shouldTrackAccountToRepair() {
assertThat(syncState.getAccountToRepair()).isEmpty();
Address testAddress = Address.fromHexString("0x1234567890123456789012345678901234567890");
syncState.markAccountToRepair(Optional.of(testAddress));
assertThat(syncState.getAccountToRepair()).isPresent();
assertThat(syncState.getAccountToRepair().get()).isEqualTo(testAddress);
syncState.markAccountToRepair(Optional.empty());
assertThat(syncState.getAccountToRepair()).isEmpty();
}
@Test
public void shouldReturnFalseForTTDBeforeInitialSyncComplete() {
SyncState syncStateWithPhase = new SyncState(blockchain, ethPeers, true, Optional.empty());
syncStateWithPhase.setReachedTerminalDifficulty(true);
assertThat(syncStateWithPhase.hasReachedTerminalDifficulty()).isPresent();
assertThat(syncStateWithPhase.hasReachedTerminalDifficulty().get()).isFalse();
}
@Test
public void shouldReturnTTDStatusAfterInitialSyncComplete() {
SyncState syncStateWithPhase = new SyncState(blockchain, ethPeers, true, Optional.empty());
syncStateWithPhase.setReachedTerminalDifficulty(true);
syncStateWithPhase.markInitialSyncPhaseAsDone();
assertThat(syncStateWithPhase.hasReachedTerminalDifficulty()).isPresent();
assertThat(syncStateWithPhase.hasReachedTerminalDifficulty().get()).isTrue();
}
@Test
public void shouldNotifyMultipleSyncProgressUpdates() {
SyncStatusListener listener = mock(SyncStatusListener.class);
syncState.subscribeSyncStatus(listener);
syncState.setSyncProgress(0, 10, 100);
syncState.setSyncProgress(0, 50, 100);
syncState.setSyncProgress(0, 100, 100);
verify(listener, times(3)).onSyncStatusChanged(syncStatusCaptor.capture());
List<Optional<SyncStatus>> statuses = syncStatusCaptor.getAllValues();
assertThat(statuses.get(0).get().getCurrentBlock()).isEqualTo(10);
assertThat(statuses.get(1).get().getCurrentBlock()).isEqualTo(50);
assertThat(statuses.get(2).get().getCurrentBlock()).isEqualTo(100);
}
@Test
public void shouldReturnLocalChainHeight() {
assertThat(syncState.getLocalChainHeight()).isEqualTo(OUR_CHAIN_HEAD_NUMBER);
advanceLocalChain(OUR_CHAIN_HEAD_NUMBER + 10);
assertThat(syncState.getLocalChainHeight()).isEqualTo(OUR_CHAIN_HEAD_NUMBER + 10);
}
@Test
public void shouldReturnBestPeerChainHead() {
updateChainState(syncTargetPeer.getEthPeer(), TARGET_CHAIN_HEIGHT, TARGET_DIFFICULTY);
doReturn(Optional.of(syncTargetPeer.getEthPeer())).when(ethPeers).bestPeerWithHeightEstimate();
Optional<ChainHeadEstimate> bestPeer = syncState.getBestPeerChainHead();
assertThat(bestPeer).isPresent();
assertThat(bestPeer.get().getEstimatedHeight()).isEqualTo(TARGET_CHAIN_HEIGHT);
}
@Test
public void shouldReturnEmptyBestPeerChainHeadWhenNoPeers() {
syncTargetPeer.disconnect(DisconnectReason.REQUESTED);
otherPeer.disconnect(DisconnectReason.REQUESTED);
Optional<ChainHeadEstimate> bestPeer = syncState.getBestPeerChainHead();
assertThat(bestPeer).isEmpty();
}
}

View File

@ -0,0 +1,362 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.ethereum.eth.transactions.layered;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.ethereum.core.TransactionTestFixture.createSignedCodeDelegation;
import static org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.MAX_SCORE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.crypto.KeyPair;
import org.hyperledger.besu.crypto.SignatureAlgorithm;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.BlobType;
import org.hyperledger.besu.datatypes.CodeDelegation;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.BlobTestFixture;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.TransactionTestFixture;
import org.hyperledger.besu.ethereum.core.Util;
import org.hyperledger.besu.ethereum.core.kzg.BlobProofBundle;
import org.hyperledger.besu.ethereum.core.kzg.BlobsWithCommitments;
import org.hyperledger.besu.ethereum.eth.manager.EthScheduler;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics;
import org.hyperledger.besu.ethereum.util.TrustedSetupClassLoaderExtension;
import org.hyperledger.besu.evm.account.Account;
import org.hyperledger.besu.metrics.StubMetricsSystem;
import org.hyperledger.besu.testutil.DeterministicEthScheduler;
import java.math.BigInteger;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.stream.IntStream;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.BeforeEach;
public class BaseTransactionPoolTest extends TrustedSetupClassLoaderExtension {
protected static final SignatureAlgorithm SIGNATURE_ALGORITHM =
SignatureAlgorithmFactory.getInstance();
protected static final KeyPair KEYS1 = SIGNATURE_ALGORITHM.generateKeyPair();
protected static final KeyPair KEYS2 = SIGNATURE_ALGORITHM.generateKeyPair();
protected static final Address SENDER1 = Util.publicKeyToAddress(KEYS1.getPublicKey());
protected static final Address SENDER2 = Util.publicKeyToAddress(KEYS2.getPublicKey());
protected static final CodeDelegation CODE_DELEGATION_SENDER_1 =
createSignedCodeDelegation(BigInteger.ONE, Address.ZERO, 0, KEYS1);
protected static final Wei DEFAULT_MIN_GAS_PRICE = Wei.of(50);
protected static final Wei DEFAULT_MIN_PRIORITY_FEE = Wei.ZERO;
private static final Random randomizeTxType = new Random();
protected final Transaction transaction0 = createTransaction(0);
protected final Transaction transaction1 = createTransaction(1);
protected final Transaction transaction2 = createTransaction(2);
protected final Transaction blobTransaction0 = createEIP4844Transaction(0, KEYS1, 1, 1);
protected final EthScheduler ethScheduler = new DeterministicEthScheduler();
protected final StubMetricsSystem metricsSystem = new StubMetricsSystem();
protected final SenderBalanceChecker senderBalanceChecker = mock(SenderBalanceChecker.class);
@BeforeEach
public void setupSenderBalanceChecker() {
when(senderBalanceChecker.hasEnoughBalanceFor(any())).thenReturn(true);
}
protected Transaction createTransaction(final long nonce) {
return createTransaction(nonce, Wei.of(5000L), KEYS1);
}
protected Transaction createTransaction(final long nonce, final KeyPair keys) {
return createTransaction(nonce, Wei.of(5000L), keys);
}
protected Transaction createTransaction(final long nonce, final Wei maxGasPrice) {
return createTransaction(nonce, maxGasPrice, KEYS1);
}
protected Transaction createTransaction(final long nonce, final int payloadSize) {
return createTransaction(nonce, Wei.of(5000L), payloadSize, KEYS1);
}
protected Transaction createTransaction(
final long nonce, final Wei maxGasPrice, final KeyPair keys) {
return createTransaction(nonce, maxGasPrice, 0, keys);
}
protected Transaction createEIP1559Transaction(
final long nonce, final KeyPair keys, final int gasFeeMultiplier) {
return createTransaction(
TransactionType.EIP1559, nonce, Wei.of(5000L).multiply(gasFeeMultiplier), 0, null, keys);
}
protected Transaction createEIP4844Transaction(
final long nonce, final KeyPair keys, final int gasFeeMultiplier, final int blobCount) {
return createTransaction(
TransactionType.BLOB,
nonce,
Wei.of(5000L).multiply(gasFeeMultiplier),
Wei.of(5000L).multiply(gasFeeMultiplier).divide(10),
0,
blobCount,
BlobType.KZG_PROOF,
null,
keys);
}
protected Transaction createEIP7702Transaction(
final long nonce,
final KeyPair keys,
final int gasFeeMultiplier,
final List<CodeDelegation> codeDelegations) {
return createTransaction(
TransactionType.DELEGATE_CODE,
nonce,
Wei.of(5000L).multiply(gasFeeMultiplier),
0,
codeDelegations,
keys);
}
protected Transaction createTransactionOfSize(
final long nonce, final Wei maxGasPrice, final int txSize, final KeyPair keys) {
final TransactionType txType =
TransactionType.values()[
randomizeTxType.nextInt(txSize < blobTransaction0.getSizeForAnnouncement() ? 3 : 4)];
final Transaction baseTx =
createTransaction(
txType,
nonce,
maxGasPrice,
maxGasPrice.divide(10),
0,
1,
BlobType.KZG_PROOF,
null,
keys);
final int payloadSize = txSize - baseTx.getSizeForAnnouncement();
return createTransaction(
txType,
nonce,
maxGasPrice,
maxGasPrice.divide(10),
payloadSize,
1,
BlobType.KZG_PROOF,
null,
keys);
}
protected Transaction createTransaction(
final long nonce, final Wei maxGasPrice, final int payloadSize, final KeyPair keys) {
final TransactionType txType = TransactionType.values()[randomizeTxType.nextInt(4)];
return switch (txType) {
case FRONTIER, ACCESS_LIST, EIP1559 ->
createTransaction(txType, nonce, maxGasPrice, payloadSize, null, keys);
case BLOB ->
createTransaction(
txType,
nonce,
maxGasPrice,
maxGasPrice.divide(10),
payloadSize,
1,
BlobType.KZG_PROOF,
null,
keys);
case DELEGATE_CODE ->
createTransaction(
txType, nonce, maxGasPrice, payloadSize, List.of(CODE_DELEGATION_SENDER_1), keys);
case AERE_PQ ->
throw new IllegalArgumentException(
"AIP-20 transactions need ML-DSA keys and are not generated by this test");
};
}
protected Transaction createTransaction(
final TransactionType type,
final long nonce,
final Wei maxGasPrice,
final int payloadSize,
final List<CodeDelegation> codeDelegations,
final KeyPair keys) {
return createTransaction(
type,
nonce,
maxGasPrice,
maxGasPrice.divide(10),
payloadSize,
0,
BlobType.KZG_PROOF,
codeDelegations,
keys);
}
protected Transaction createTransaction(
final TransactionType type,
final long nonce,
final Wei maxGasPrice,
final Wei maxPriorityFeePerGas,
final int payloadSize,
final int blobCount,
final BlobType blobType,
final List<CodeDelegation> codeDelegations,
final KeyPair keys) {
return prepareTransaction(
type,
nonce,
maxGasPrice,
maxPriorityFeePerGas,
payloadSize,
blobCount,
blobType,
codeDelegations)
.createTransaction(keys);
}
protected TransactionTestFixture prepareTransaction(
final TransactionType type,
final long nonce,
final Wei maxGasPrice,
final Wei maxPriorityFeePerGas,
final int payloadSize,
final int blobCount,
final BlobType blobType,
final List<CodeDelegation> codeDelegations) {
var tx =
new TransactionTestFixture()
.to(Optional.of(Address.fromHexString("0x634316eA0EE79c701c6F67C53A4C54cBAfd2316d")))
.value(Wei.of(nonce))
.nonce(nonce)
.type(type);
if (payloadSize > 0) {
var payloadBytes = Bytes.fromHexString("01".repeat(payloadSize));
tx.payload(payloadBytes);
}
if (type.supports1559FeeMarket()) {
tx.maxFeePerGas(Optional.of(maxGasPrice))
.maxPriorityFeePerGas(Optional.of(maxPriorityFeePerGas));
if (type.supportsBlob() && blobCount > 0) {
tx.maxFeePerBlobGas(Optional.of(maxGasPrice));
final var blobTestFixture = new BlobTestFixture();
final var blobProofBundles =
IntStream.range(0, blobCount)
.mapToObj(i -> blobTestFixture.createBlobProofBundle(blobType))
.toList();
final var versionedHashes =
blobProofBundles.stream().map(BlobProofBundle::getVersionedHash).toList();
final var blobsWithCommitments = new BlobsWithCommitments(blobProofBundles);
tx.versionedHashes(Optional.of(versionedHashes));
tx.blobsWithCommitments(Optional.of(blobsWithCommitments));
} else if (type.supportsDelegateCode()) {
tx.codeDelegations(codeDelegations);
}
} else {
tx.gasPrice(maxGasPrice);
}
return tx;
}
protected Transaction createTransactionReplacement(
final Transaction originalTransaction, final KeyPair keys) {
return createTransaction(
originalTransaction.getType(),
originalTransaction.getNonce(),
originalTransaction.getMaxGasPrice().multiply(2),
originalTransaction.getMaxGasPrice().multiply(2).divide(10),
0,
1,
originalTransaction
.getBlobsWithCommitments()
.map(BlobsWithCommitments::getBlobType)
.orElse(BlobType.KZG_PROOF),
originalTransaction.getCodeDelegationList().orElse(null),
keys);
}
protected PendingTransaction createRemotePendingTransaction(final Transaction transaction) {
return new PendingTransaction.Remote(transaction);
}
protected PendingTransaction createRemotePendingTransaction(
final Transaction transaction, final boolean hasPriority) {
return PendingTransaction.newPendingTransaction(transaction, false, hasPriority, MAX_SCORE);
}
protected PendingTransaction createLocalPendingTransaction(final Transaction transaction) {
return new PendingTransaction.Local(transaction);
}
protected void assertTransactionPending(
final PendingTransactions transactions, final Transaction t) {
assertThat(transactions.getTransactionByHash(t.getHash())).contains(t);
}
protected void assertTransactionNotPending(
final PendingTransactions transactions, final Transaction t) {
assertThat(transactions.getTransactionByHash(t.getHash())).isEmpty();
}
protected void assertNoNextNonceForSender(
final PendingTransactions pendingTransactions, final Address sender) {
assertThat(pendingTransactions.getNextNonceForSender(sender)).isEmpty();
}
protected void assertNextNonceForSender(
final PendingTransactions pendingTransactions, final Address sender1, final int i) {
assertThat(pendingTransactions.getNextNonceForSender(sender1)).isPresent().hasValue(i);
}
protected void addLocalTransactions(
final PendingTransactions sorter, final Account sender, final long... nonces) {
for (final long nonce : nonces) {
sorter.addTransaction(
createLocalPendingTransaction(createTransaction(nonce)), Optional.of(sender));
}
}
protected long getAddedCount(
final String source, final String priority, final AddReason addReason, final String layer) {
return metricsSystem.getCounterValue(
TransactionPoolMetrics.ADDED_COUNTER_NAME, source, priority, addReason.label(), layer);
}
protected long getRemovedCount(
final String source, final String priority, final String operation, final String layer) {
return metricsSystem.getCounterValue(
TransactionPoolMetrics.REMOVED_COUNTER_NAME, source, priority, operation, layer);
}
}

View File

@ -0,0 +1,435 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-bft-bounded-message-lists.patch.
*/
package org.hyperledger.besu.ethereum.rlp;
import java.math.BigInteger;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.bytes.Bytes48;
import org.apache.tuweni.units.bigints.UInt256;
import org.apache.tuweni.units.bigints.UInt64;
/**
* An input used to decode data in RLP encoding.
*
* <p>An RLP "value" is fundamentally an {@code Item} defined the following way:
*
* <pre>
* Item ::= List | Bytes
* List ::= [ Item, ... , Item ]
* Bytes ::= a binary value (comprised of an arbitrary number of bytes).
* </pre>
*
* <p>In other words, RLP encodes binary data organized in arbitrary nested lists.
*
* <p>A {@link RLPInput} thus provides methods to decode both lists and binary values. A list in the
* input is "entered" by calling {@link #enterList()} and left by calling {@link #leaveList()}.
* Binary values can be read directly with {@link #readBytes()}, but the {@link RLPInput} interface
* provides a wealth of convenience methods to read specific types of data that are in specific
* encoding.
*
* <p>Amongst the methods to read binary data, some methods are provided to read "scalar". A scalar
* should simply be understood as a positive integer that is encoded with no leading zeros. In other
* word, a method like {@link #readLongScalar()} does not expect an encoded value of exactly 8 bytes
* (by opposition to {@link #readLong}), but rather one that is "up to" 8 bytes.
*
* @see BytesValueRLPInput
*/
public interface RLPInput {
/**
* Whether the input has been already fully decoded (has no more data to read).
*
* @return {@code false} if the input has more data to read, {@code true} otherwise.
*/
boolean isDone();
/**
* Whether the next element to read from this input is a list.
*
* @return {@code true} if the input is not done and the next item to read is a list.
*/
boolean nextIsList();
/**
* Whether the next element to read from this input is an RLP "null" (that is, {@link
* Bytes#EMPTY}).
*
* @return {@code true} if the input is not done and the next item to read is an empty value.
*/
boolean nextIsNull();
/**
* Returns the payload size of the next item
*
* @return the payload size of the next item
*/
int nextSize();
/**
* Returns the size of the current item
*
* @return the size of the current item
*/
int currentSize();
/**
* Returns the offset of the next item, counting from the start of the RLP Input as a whole.
*
* @return offset from buffer start
*/
int nextOffset();
/**
* Whether the input is at the end of a currently entered list, that is if {@link #leaveList()}
* should be the next method called.
*
* @return Whether all elements of the current list have been read but said list haven't been
* "left" yet.
*/
boolean isEndOfCurrentList();
/**
* Skips the next item to read in the input.
*
* <p>Note that if the next item is a list, the whole list is skipped.
*/
void skipNext();
/**
* If the next item to read is a list, enter that list, placing the input on the first item of
* that list.
*
* @return The number of item of the entered list.
* @throws RLPException if the next item to read from this input is not a list, or the input is
* corrupted.
*/
int enterList();
/**
* Exits the current list after all its items have been consumed.
*
* <p>Note that this method technically doesn't consume any input but must be called after having
* read the last element of a list. This allows it to ensure the structure of the input is indeed
* the one expected.
*
* @throws RLPException if the current list is not finished (it has more items).
*/
void leaveList();
/**
* Exits the current list, ignoring any remaining unconsumed elements.
*
* <p>Note that this method technically doesn't consume any input but must be called after having
* read the last element of a list. This allows it to ensure the structure of the input is indeed
* the one expected.
*/
void leaveListLenient();
/**
* Reads a non-negative scalar from the input and return it as a long value which is interpreted
* as an unsigned long.
*
* @return The next scalar item of this input as a long value.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to
* fit a long or has leading zeros.
*/
long readLongScalar();
/**
* Reads a scalar from the input and return is as an int value.
*
* @return The next scalar item of this input as an int value.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to
* fit a long or has leading zeros.
*/
int readIntScalar();
/**
* Reads a scalar from the input and return is as a {@link BigInteger}.
*
* @return The next scalar item of this input as a {@link BigInteger}.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item has leading zeros.
*/
BigInteger readBigIntegerScalar();
/**
* Reads a scalar from the input and return is as a {@link UInt64}.
*
* @return The next scalar item of this input as a {@link UInt64}.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to
* fit a {@link UInt64} or has leading zeros.
*/
UInt64 readUInt64Scalar();
/**
* Reads a scalar from the input and return is as a {@link UInt256}.
*
* @return The next scalar item of this input as a {@link UInt256}.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to
* fit a {@link UInt256} or has leading zeros.
*/
UInt256 readUInt256Scalar();
/**
* Reads the next item of this input (which must be exactly 1 byte) as a byte.
*
* @return The byte corresponding to the next item of this input.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is not a single byte
* long.
*/
byte readByte();
/**
* Reads the next item of this input (which must be exactly 2-bytes) as a (signed) short.
*
* @return The short corresponding to the next item of this input.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is not 2-bytes.
*/
short readShort();
/**
* Reads the next item of this input (which must be exactly 4-bytes) as a (signed) int.
*
* @return The int corresponding to the next item of this input.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is not 4-bytes.
*/
int readInt();
/**
* Reads the next item of this input (which must be exactly 8-bytes) as a (signed) long.
*
* @return The long corresponding to the next item of this input.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is not 8-bytes.
*/
long readLong();
/**
* Reads the next item of this input (which must be exactly 1 byte) as an unsigned byte.
*
* @return The value of the next item interpreted as an unsigned byte.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is not a single byte
* long.
*/
default int readUnsignedByte() {
if (isZeroLengthString()) {
// Decode an empty string (0x80) as an unsigned byte with value 0
readBytes();
return 0;
}
return readByte() & 0xFF;
}
/**
* Reads the next item of this input (which must be exactly 2-bytes) as an unsigned short.
*
* @return The value of the next item interpreted as an unsigned short.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is not 2-bytes.
*/
default int readUnsignedShort() {
return readShort() & 0xFFFF;
}
/**
* Reads the next item of this input (which must be exactly 4-bytes) as an unsigned int.
*
* @return The value of the next item interpreted as an unsigned int.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is not 4-bytes.
*/
default long readUnsignedInt() {
return (readInt()) & 0xFFFFFFFFL;
}
/**
* Reads a scalar from the input and return is as an unsigned int contained in a long
*
* @return The next scalar item of this input as an unsigned int value as long
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to
* fit an unsigned int or has leading zeros.
*/
long readUnsignedIntScalar();
/**
* Reads a scalar from the input and return is as an unsigned byte contained in an int
*
* @return The next scalar item of this input as an unsigned byte value as int
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to
* fit an unsigned byte or has leading zeros.
*/
int readUnsignedByteScalar();
/**
* Reads an inet address from this input.
*
* @return The inet address corresponding to the next item of this input.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or if the next item is neither 4 nor 16
* bytes.
*/
InetAddress readInetAddress();
/**
* Reads the next item of this input (assuming it is not a list).
*
* @return The next item read of this input.
* @throws RLPException if the next item to read is a list or the input is at the end of its
* current list (and {@link #leaveList()} hasn't been called).
*/
Bytes readBytes();
/**
* Reads the next item of this input (assuming it is not a list) that must be exact 32 bytes.
*
* @return The next item read of this input.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or the next element is not exactly 32
* bytes.
*/
Bytes32 readBytes32();
/**
* Reads the next item of this input (assuming it is not a list) that must be exact 48 bytes.
*
* @return The next item read of this input.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or the next element is not exactly 48
* bytes.
*/
Bytes48 readBytes48();
/**
* Reads the next item of this input (assuming it is not a list) and transforms it with the
* provided mapping function.
*
* <p>Note that the only benefit of this method over calling the mapper function on the result of
* {@link #readBytes()} is that any error thrown by the mapper will be wrapped by a {@link
* RLPException}, which can make error handling more convenient (having a particular decoded value
* not match what is expected is not fundamentally different from trying to read an unsigned short
* from an item with strictly more or less than 2 bytes).
*
* @param mapper The mapper to apply to the read value.
* @param <T> The type of the result.
* @return The next item read from this input, mapped through {@code mapper}.
* @throws RLPException if the next item to read is a list, the input is at the end of its current
* list (and {@link #leaveList()} hasn't been called) or {@code mapper} throws an exception
* when applied.
*/
<T> T readBytes(Function<Bytes, T> mapper);
/**
* Returns the current element as a standalone RLP element.
*
* <p>This method is useful to extract self-contained RLP elements from a list, so they can be
* processed individually later.
*
* @return The current element as a standalone RLP input element.
*/
RLPInput readAsRlp();
/**
* Returns a raw {@link Bytes} representation of this RLP.
*
* @return The raw RLP.
*/
Bytes raw();
boolean isZeroLengthString();
/** Resets this RLP input to the start. */
void reset();
/**
* Returns a raw {@link Bytes} representation of this RLP list.
*
* @return The raw RLP list.
*/
Bytes currentListAsBytes();
/**
* Reads a full list from the input given a method that knows how to read its elements.
*
* @param valueReader A method that can decode a single list element.
* @param <T> The type of the elements of the decoded list.
* @return The next list of this input, where elements are decoded using {@code valueReader}.
* @throws RLPException is the next item to read is not a list, of if any error happens when
* applying {@code valueReader} to read elements of the list.
*/
default <T> List<T> readList(final Function<RLPInput, T> valueReader) {
return readList(valueReader, Integer.MAX_VALUE);
}
/**
* Reads a full list from the input, rejecting inputs whose element count exceeds {@code
* maxElements}. AERE D-434: ported from upstream hyperledger/besu dd4101c9ec.
*
* @param valueReader A method that can decode a single list element.
* @param maxElements The maximum number of elements the list may contain.
* @param <T> The type of the elements of the decoded list.
* @return The next list of this input, where elements are decoded using {@code valueReader}.
* @throws RLPException if the next item is not a list, if it holds more than {@code maxElements}
* elements, or if any error happens when applying {@code valueReader} to read elements.
*/
default <T> List<T> readList(final Function<RLPInput, T> valueReader, final int maxElements) {
final int size = enterList();
if (size > maxElements) {
throw new RLPException(
String.format(
"List of %d elements exceeds the maximum permitted size of %d", size, maxElements));
}
final List<T> res = size == 0 ? List.of() : new ArrayList<>(size);
for (int i = 0; i < size; i++) {
try {
res.add(valueReader.apply(this));
} catch (final RLPException e) {
throw e;
} catch (final Exception e) {
throw new RLPException(
String.format("Error applying element decoding function on element %d of the list", i),
e);
}
}
leaveList();
return res;
}
default Bytes currentListAsBytesNoCopy(final boolean moveToNextItem) {
throw new UnsupportedOperationException("Not supported by this input");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,152 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-warm-access-hashdos-treeset.patch.
*/
package org.hyperledger.besu.evm.frame;
import org.hyperledger.besu.collections.undo.UndoScalar;
import org.hyperledger.besu.collections.undo.UndoSet;
import org.hyperledger.besu.collections.undo.UndoTable;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.VersionedHash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.evm.blockhash.BlockHashLookup;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.TreeSet;
import java.util.List;
import java.util.Optional;
import com.google.common.collect.TreeBasedTable;
import org.apache.tuweni.bytes.Bytes32;
/**
* Transaction Values used by various EVM Opcodes. These are the values that either do not change or
* the backing stores whose changes transcend message frames and are not part of state, such as
* transient storage and address warming.
*
* @param blockHashLookup The block hash lookup function
* @param maxStackSize The maximum stack size
* @param warmedUpAddresses The warmed-up addresses
* @param warmedUpStorage The warmed-up storage
* @param originator The originator address
* @param gasPrice The gas price
* @param blobGasPrice The blob gas price
* @param blockValues The block values
* @param messageFrameStack The message frame stack
* @param miningBeneficiary The mining beneficiary address
* @param versionedHashes The optional list of versioned hashes
* @param transientStorage The transient storage
* @param creates The set of addresses that creates
* @param selfDestructs The set of addresses that self-destructs
* @param gasRefunds The gas refunds
* @param stateGasUsed The cumulative state gas used (EIP-8037), undone on revert
* @param stateGasReservoir The EIP-8037 state gas reservoir (overflow from regular gas budget),
* undone on revert
* @param stateGasSpillBurned EIP-8037 accumulated state gas that spilled from reverted child
* frames; NOT undone on revert (permanent burn counter for block accounting)
* @param initialFrameRegularHaltBurn EIP-7778/EIP-8037 gas burned when the initial frame halts
* exceptionally (gasRemaining at halt time). Paid by the sender via receipts, but must be
* excluded from block regular gas. Single-element long[] so it is NOT undone on revert.
*/
public record TxValues(
BlockHashLookup blockHashLookup,
int maxStackSize,
UndoSet<Address> warmedUpAddresses,
UndoTable<Address, Bytes32, Boolean> warmedUpStorage,
Address originator,
Wei gasPrice,
Wei blobGasPrice,
BlockValues blockValues,
Deque<MessageFrame> messageFrameStack,
Address miningBeneficiary,
Optional<List<VersionedHash>> versionedHashes,
UndoTable<Address, Bytes32, Bytes32> transientStorage,
UndoSet<Address> creates,
UndoSet<Address> selfDestructs,
UndoScalar<Long> gasRefunds,
UndoScalar<Long> stateGasUsed,
UndoScalar<Long> stateGasReservoir,
long[] stateGasSpillBurned,
long[] initialFrameRegularHaltBurn) {
/**
* Creates a new TxValues for the initial (depth-0) frame of a transaction. EIP-8037 gas tracking
* fields are initialized to zero.
*
* @param blockHashLookup block hash lookup function
* @param maxStackSize maximum stack size
* @param warmedUpAddresses pre-warmed addresses
* @param originator the transaction originator
* @param gasPrice the gas price
* @param blobGasPrice the blob gas price
* @param blockValues the block values
* @param miningBeneficiary the mining beneficiary
* @param versionedHashes optional versioned hashes
* @return a new TxValues instance
*/
public static TxValues forTransaction(
final BlockHashLookup blockHashLookup,
final int maxStackSize,
final UndoSet<Address> warmedUpAddresses,
final Address originator,
final Wei gasPrice,
final Wei blobGasPrice,
final BlockValues blockValues,
final Address miningBeneficiary,
final Optional<List<VersionedHash>> versionedHashes) {
return new TxValues(
blockHashLookup,
maxStackSize,
warmedUpAddresses,
UndoTable.of(TreeBasedTable.create()),
originator,
gasPrice,
blobGasPrice,
blockValues,
new ArrayDeque<>(),
miningBeneficiary,
versionedHashes,
UndoTable.of(TreeBasedTable.create()),
UndoSet.of(new TreeSet<>()),
UndoSet.of(new TreeSet<>()),
new UndoScalar<>(0L),
new UndoScalar<>(0L),
new UndoScalar<>(0L),
new long[] {0L},
new long[] {0L});
}
/**
* For all data stored in this record, undo the changes since the mark.
*
* @param mark the mark to which it should be rolled back to
*/
public void undoChanges(final long mark) {
warmedUpAddresses.undo(mark);
warmedUpStorage.undo(mark);
transientStorage.undo(mark);
creates.undo(mark);
selfDestructs.undo(mark);
gasRefunds.undo(mark);
stateGasUsed.undo(mark);
stateGasReservoir.undo(mark);
}
}

View File

@ -0,0 +1,611 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0006-aere-pq-transactions.patch.
*/
package org.hyperledger.besu.evm.gascalculator;
import static org.hyperledger.besu.evm.internal.Words.clampedAdd;
import static org.hyperledger.besu.evm.internal.Words.clampedMultiply;
import static org.hyperledger.besu.evm.internal.Words.clampedToInt;
import static org.hyperledger.besu.evm.internal.Words.numWords;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Transaction;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.evm.account.Account;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.operation.ExpOperation;
import java.util.function.Supplier;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
/** The Frontier gas calculator. */
public class FrontierGasCalculator implements GasCalculator {
private static final long TX_DATA_ZERO_COST = 4L;
private static final long TX_DATA_NON_ZERO_COST = 68L;
/** Minimum base cost that every transaction needs to pay */
protected static final long TX_BASE_COST = 21_000L;
private static final long TX_CREATE_EXTRA_COST = 0L;
private static final long CODE_DEPOSIT_BYTE_COST = 200L;
private static final long ID_PRECOMPILED_BASE_GAS_COST = 15L;
private static final long ID_PRECOMPILED_WORD_GAS_COST = 3L;
private static final long ECREC_PRECOMPILED_GAS_COST = 3_000L;
private static final long P256VERIFY_PRECOMPILED_GAS_COST = 6_900L;
private static final long SHA256_PRECOMPILED_BASE_GAS_COST = 60L;
private static final long SHA256_PRECOMPILED_WORD_GAS_COST = 12L;
private static final long RIPEMD160_PRECOMPILED_WORD_GAS_COST = 120L;
private static final long RIPEMD160_PRECOMPILED_BASE_GAS_COST = 600L;
private static final long VERY_LOW_TIER_GAS_COST = 3L;
private static final long LOW_TIER_GAS_COST = 5L;
private static final long BASE_TIER_GAS_COST = 2L;
private static final long MID_TIER_GAS_COST = 8L;
private static final long HIGH_TIER_GAS_COST = 10L;
private static final long CALL_OPERATION_BASE_GAS_COST = 40L;
private static final long CALL_VALUE_TRANSFER_GAS_COST = 9_000L;
/** additional call stipend to be used for gas estimate calculations */
public static final long ADDITIONAL_CALL_STIPEND = 2_300L;
private static final long NEW_ACCOUNT_GAS_COST = 25_000L;
/** Yellow paper constant for the cost of creating a new contract on-chain */
protected static final long CREATE_OPERATION_GAS_COST = 32_000L;
private static final long COPY_WORD_GAS_COST = 3L;
private static final long MEMORY_WORD_GAS_COST = 3L;
private static final long BALANCE_OPERATION_GAS_COST = 20L;
/** The constant BLOCKHASH_OPERATION_GAS_COST = 20gwei. */
public static final long BLOCKHASH_OPERATION_GAS_COST = 20L;
private static final long EXP_OPERATION_BASE_GAS_COST = 10L;
private static final long EXP_OPERATION_BYTE_GAS_COST = 10L;
private static final long EXT_CODE_BASE_GAS_COST = 20L;
private static final long JUMPDEST_OPERATION_GAS_COST = 1L;
private static final long LOG_OPERATION_BASE_GAS_COST = 375L;
private static final long LOG_OPERATION_DATA_BYTE_GAS_COST = 8L;
private static final long LOG_OPERATION_TOPIC_GAS_COST = 375L;
private static final long KECCAK256_OPERATION_BASE_GAS_COST = 30L;
/** The constant KECCAK256_OPERATION_WORD_GAS_COST. */
static final long KECCAK256_OPERATION_WORD_GAS_COST = 6L;
private static final long SLOAD_OPERATION_GAS_COST = 50L;
/** The constant STORAGE_SET_GAS_COST. */
public static final long STORAGE_SET_GAS_COST = 20_000L;
/** The constant STORAGE_RESET_GAS_COST. */
public static final long STORAGE_RESET_GAS_COST = 5_000L;
/** The constant STORAGE_RESET_REFUND_AMOUNT. */
public static final long STORAGE_RESET_REFUND_AMOUNT = 15_000L;
private static final long SELF_DESTRUCT_REFUND_AMOUNT = 24_000L;
/** Default constructor. */
public FrontierGasCalculator() {
// Default Constructor, for JavaDoc lint
}
@Override
public long transactionIntrinsicGasCost(final Transaction transaction, final long baselineGas) {
final long dynamicIntrinsicGasCost = dynamicIntrinsicGasCost(transaction, baselineGas);
if (dynamicIntrinsicGasCost == Long.MIN_VALUE || dynamicIntrinsicGasCost == Long.MAX_VALUE) {
return dynamicIntrinsicGasCost;
}
return clampedAdd(getMinimumTransactionCost(), dynamicIntrinsicGasCost);
}
/**
* Calculates the dynamic part of the intrinsic gas cost
*
* @param transaction the transaction
* @param baselineGas how much gas is used by access lists and code delegations
* @return the dynamic part of the intrinsic gas cost
*/
protected long dynamicIntrinsicGasCost(final Transaction transaction, final long baselineGas) {
final int payloadSize = transaction.getPayload().size();
long cost =
clampedAdd(callDataCost(payloadSize, transaction.getPayloadZeroBytes()), baselineGas);
if (cost == Long.MIN_VALUE || cost == Long.MAX_VALUE) {
return cost;
}
// AIP-20: the post-quantum authorization bytes are charged like non-zero calldata (16 per byte)
// and the verification itself at the algorithm's flat cost. Zero for every other type.
final long pqCost =
transaction
.getPqAuthorization()
.map(a -> clampedAdd(16L * a.authorizationByteCount(), a.algorithm().verifyGas()))
.orElse(0L);
cost = clampedAdd(cost, pqCost);
if (cost == Long.MIN_VALUE || cost == Long.MAX_VALUE) {
return cost;
}
if (transaction.isContractCreation()) {
cost = clampedAdd(cost, contractCreationCost(payloadSize));
if (cost == Long.MIN_VALUE || cost == Long.MAX_VALUE) {
return cost;
}
}
return cost;
}
/**
* Calculates the cost of the call data
*
* @param payloadSize the total size of the payload
* @param zeroBytes the number of zero bytes in the payload
* @return the cost of the call data
*/
protected long callDataCost(final long payloadSize, final long zeroBytes) {
return clampedAdd(
TX_DATA_NON_ZERO_COST * (payloadSize - zeroBytes), TX_DATA_ZERO_COST * zeroBytes);
}
/**
* Returns the gas cost for contract creation transactions
*
* @param ignored the size of the contract creation code (ignored in Frontier)
* @return the gas cost for contract creation transactions
*/
protected long contractCreationCost(final int ignored) {
return txCreateExtraGasCost();
}
/**
* Returns the additional gas cost for contract creation transactions
*
* @return the additional gas cost for contract creation transactions
*/
protected long txCreateExtraGasCost() {
return TX_CREATE_EXTRA_COST;
}
@Override
public long transactionFloorCost(final Bytes payload, final long payloadZeroBytes) {
return 0L;
}
@Override
public long codeDepositGasCost(final int codeSize) {
return CODE_DEPOSIT_BYTE_COST * codeSize;
}
@Override
public long idPrecompiledContractGasCost(final Bytes input) {
return ID_PRECOMPILED_WORD_GAS_COST * numWords(input) + ID_PRECOMPILED_BASE_GAS_COST;
}
@Override
public long getEcrecPrecompiledContractGasCost() {
return ECREC_PRECOMPILED_GAS_COST;
}
@Override
public long getP256VerifyPrecompiledContractGasCost() {
return P256VERIFY_PRECOMPILED_GAS_COST;
}
@Override
public long sha256PrecompiledContractGasCost(final Bytes input) {
return SHA256_PRECOMPILED_WORD_GAS_COST * numWords(input) + SHA256_PRECOMPILED_BASE_GAS_COST;
}
@Override
public long ripemd160PrecompiledContractGasCost(final Bytes input) {
return RIPEMD160_PRECOMPILED_WORD_GAS_COST * numWords(input)
+ RIPEMD160_PRECOMPILED_BASE_GAS_COST;
}
@Override
public long getZeroTierGasCost() {
return 0L;
}
@Override
public long getVeryLowTierGasCost() {
return VERY_LOW_TIER_GAS_COST;
}
@Override
public long getLowTierGasCost() {
return LOW_TIER_GAS_COST;
}
@Override
public long getBaseTierGasCost() {
return BASE_TIER_GAS_COST;
}
@Override
public long getMidTierGasCost() {
return MID_TIER_GAS_COST;
}
@Override
public long getHighTierGasCost() {
return HIGH_TIER_GAS_COST;
}
/**
* Returns the base gas cost to execute a call operation.
*
* @return the base gas cost to execute a call operation
*/
@Override
public long callOperationBaseGasCost() {
return CALL_OPERATION_BASE_GAS_COST;
}
@Override
public long callValueTransferGasCost() {
return CALL_VALUE_TRANSFER_GAS_COST;
}
@Override
public long newAccountGasCost() {
return NEW_ACCOUNT_GAS_COST;
}
@Override
public long callOperationStaticGasCost(
final MessageFrame frame,
final long stipend,
final long inputDataOffset,
final long inputDataLength,
final long outputDataOffset,
final long outputDataLength,
final Wei transferValue,
final Address recipientAddress,
final boolean accountIsWarm) {
final long inputDataMemoryExpansionCost =
memoryExpansionGasCost(frame, inputDataOffset, inputDataLength);
final long outputDataMemoryExpansionCost =
memoryExpansionGasCost(frame, outputDataOffset, outputDataLength);
final long memoryExpansionCost =
Math.max(inputDataMemoryExpansionCost, outputDataMemoryExpansionCost);
long cost = clampedAdd(clampedAdd(callOperationBaseGasCost(), stipend), memoryExpansionCost);
if (!transferValue.isZero()) {
cost = clampedAdd(cost, callValueTransferGasCost());
}
return cost;
}
@Override
public long callOperationGasCost(
final MessageFrame frame,
final long staticCallCost,
final long stipend,
final long inputDataOffset,
final long inputDataLength,
final long outputDataOffset,
final long outputDataLength,
final Wei transferValue,
final Address recipientAddress,
final boolean accountIsWarm) {
long cost = staticCallCost;
final Account recipient = frame.getWorldUpdater().get(recipientAddress);
if (recipient == null) {
cost = clampedAdd(cost, newAccountGasCost());
}
return cost;
}
/**
* Returns the additional call stipend for calls with value transfers.
*
* @return the additional call stipend for calls with value transfers
*/
@Override
public long getAdditionalCallStipend() {
return ADDITIONAL_CALL_STIPEND;
}
@Override
public long gasAvailableForChildCall(
final MessageFrame frame, final long stipend, final boolean transfersValue) {
if (transfersValue) {
return stipend + getAdditionalCallStipend();
} else {
return stipend;
}
}
@Override
public long getMinRetainedGas() {
return 0;
}
@Override
public long getMinCalleeGas() {
return 0;
}
@Override
public long txCreateCost() {
return CREATE_OPERATION_GAS_COST;
}
@Override
public long createKeccakCost(final int initCodeLength) {
return clampedMultiply(KECCAK256_OPERATION_WORD_GAS_COST, numWords(initCodeLength));
}
@Override
public long initcodeCost(final int initCodeLength) {
return 0;
}
@Override
public long gasAvailableForChildCreate(final long stipend) {
return stipend;
}
@Override
public long dataCopyOperationGasCost(
final MessageFrame frame, final long offset, final long length) {
return copyWordsToMemoryGasCost(
frame, VERY_LOW_TIER_GAS_COST, COPY_WORD_GAS_COST, offset, length);
}
@Override
public long memoryExpansionGasCost(
final MessageFrame frame, final long offset, final long length) {
final long pre = memoryCost(frame.memoryWordSize());
final long post = memoryCost(frame.calculateMemoryExpansion(offset, length));
if (post == Long.MAX_VALUE) {
return Long.MAX_VALUE;
}
return post - pre;
}
@Override
public long getBalanceOperationGasCost() {
return BALANCE_OPERATION_GAS_COST;
}
@Override
public long getBlockHashOperationGasCost() {
return BLOCKHASH_OPERATION_GAS_COST;
}
/**
* Returns the gas cost for a byte in the {@link ExpOperation}.
*
* @return the gas cost for a byte in the exponent operation
*/
protected long expOperationByteGasCost() {
return EXP_OPERATION_BYTE_GAS_COST;
}
@Override
public long expOperationGasCost(final int numBytes) {
return expOperationByteGasCost() * numBytes + EXP_OPERATION_BASE_GAS_COST;
}
/**
* Returns the base gas cost for external code accesses.
*
* @return the base gas cost for external code accesses
*/
protected long extCodeBaseGasCost() {
return EXT_CODE_BASE_GAS_COST;
}
@Override
public long extCodeCopyOperationGasCost(
final MessageFrame frame, final long offset, final long length) {
return copyWordsToMemoryGasCost(
frame, extCodeBaseGasCost(), COPY_WORD_GAS_COST, offset, length);
}
@Override
public long extCodeHashOperationGasCost() {
throw new UnsupportedOperationException(
"EXTCODEHASH not supported by " + getClass().getSimpleName());
}
@Override
public long getExtCodeSizeOperationGasCost() {
return extCodeBaseGasCost();
}
@Override
public long getJumpDestOperationGasCost() {
return JUMPDEST_OPERATION_GAS_COST;
}
@Override
public long logOperationGasCost(
final MessageFrame frame, final long dataOffset, final long dataLength, final int numTopics) {
return clampedAdd(
LOG_OPERATION_BASE_GAS_COST,
clampedAdd(
clampedMultiply(LOG_OPERATION_DATA_BYTE_GAS_COST, dataLength),
clampedAdd(
clampedMultiply(LOG_OPERATION_TOPIC_GAS_COST, numTopics),
memoryExpansionGasCost(frame, dataOffset, dataLength))));
}
@Override
public long mLoadOperationGasCost(final MessageFrame frame, final long offset) {
return clampedAdd(VERY_LOW_TIER_GAS_COST, memoryExpansionGasCost(frame, offset, 32));
}
@Override
public long mStoreOperationGasCost(final MessageFrame frame, final long offset) {
return clampedAdd(VERY_LOW_TIER_GAS_COST, memoryExpansionGasCost(frame, offset, 32));
}
@Override
public long mStore8OperationGasCost(final MessageFrame frame, final long offset) {
return clampedAdd(VERY_LOW_TIER_GAS_COST, memoryExpansionGasCost(frame, offset, 1));
}
@Override
public long keccak256OperationGasCost(
final MessageFrame frame, final long offset, final long length) {
return copyWordsToMemoryGasCost(
frame,
KECCAK256_OPERATION_BASE_GAS_COST,
KECCAK256_OPERATION_WORD_GAS_COST,
offset,
length);
}
@Override
public long getSloadOperationGasCost() {
return SLOAD_OPERATION_GAS_COST;
}
@Override
public long calculateStorageCost(
final UInt256 newValue,
final Supplier<UInt256> currentValue,
final Supplier<UInt256> originalValue) {
return !newValue.isZero() && currentValue.get().isZero()
? STORAGE_SET_GAS_COST
: STORAGE_RESET_GAS_COST;
}
@Override
public long calculateStorageRefundAmount(
final UInt256 newValue,
final Supplier<UInt256> currentValue,
final Supplier<UInt256> originalValue) {
return newValue.isZero() && !currentValue.get().isZero() ? STORAGE_RESET_REFUND_AMOUNT : 0L;
}
@Override
public long getSelfDestructRefundAmount() {
return SELF_DESTRUCT_REFUND_AMOUNT;
}
/**
* Copy words to memory gas cost.
*
* @param frame the frame
* @param baseGasCost the base gas cost
* @param wordGasCost the word gas cost
* @param offset the offset
* @param length the length
* @return the cost
*/
protected long copyWordsToMemoryGasCost(
final MessageFrame frame,
final long baseGasCost,
final long wordGasCost,
final long offset,
final long length) {
final long copyCost =
clampedAdd(clampedMultiply(wordGasCost, numWords(clampedToInt(length))), baseGasCost);
final long memoryCost = memoryExpansionGasCost(frame, offset, length);
return clampedAdd(copyCost, memoryCost);
}
/**
* Memory cost.
*
* @param length the length
* @return the cost
*/
static long memoryCost(final long length) {
final long lengthSquare = clampedMultiply(length, length);
final long base =
(lengthSquare == Long.MAX_VALUE)
? clampedMultiply(length / 512, length)
: lengthSquare / 512;
return clampedAdd(clampedMultiply(MEMORY_WORD_GAS_COST, length), base);
}
@Override
public long getMinimumTransactionCost() {
return TX_BASE_COST;
}
@Override
public long calculateGasRefund(
final Transaction transaction,
final MessageFrame initialFrame,
final long ignoredCodeDelegationRefund) {
final long refundAllowance = calculateRefundAllowance(transaction, initialFrame);
return initialFrame.getRemainingGas() + refundAllowance;
}
private long calculateRefundAllowance(
final Transaction transaction, final MessageFrame initialFrame) {
final long selfDestructRefund =
getSelfDestructRefundAmount() * initialFrame.getSelfDestructs().size();
final long executionRefund = initialFrame.getGasRefund() + selfDestructRefund;
// Integer truncation takes care of the floor calculation needed after the divide.
final long maxRefundAllowance =
(transaction.getGasLimit() - initialFrame.getRemainingGas()) / getMaxRefundQuotient();
return Math.min(executionRefund, maxRefundAllowance);
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.evm.precompile;
import java.util.Arrays;
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.FalconPublicKeyParameters;
import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
/**
* Shared logic for the AERE Falcon-512 / Falcon-1024 verification precompiles.
*
* <p>Transcodes the NIST round-3 reference "signed message" (sm) blob into the encoding Bouncy
* Castle's {@link FalconSigner#verifySignature(byte[], byte[])} expects and drives that
* verifier. No cryptography is reimplemented here.
*
* <p>NIST sm layout: {@code sigLen(2, big-endian) || nonce(40) || message || esig} where
* {@code esig = (0x20+logn) || compressedSig} and {@code sigLen == esig.length}. The Falcon public
* key is {@code (0x00+logn) || packed_h}; Bouncy Castle wants only {@code packed_h} (header
* stripped), and the signature it wants is {@code (0x30+logn) || nonce(40) || compressedSig}.
*/
final class AereFalconSupport {
static final int NONCE_LEN = 40;
private AereFalconSupport() {}
static Bytes resultWord(final boolean valid) {
return valid ? Bytes32.leftPad(Bytes.of((byte) 1)) : Bytes32.ZERO;
}
/**
* Verify a NIST signed-message blob against a Falcon public key.
*
* @param params Bouncy Castle Falcon parameter set (falcon_512 / falcon_1024)
* @param logn 9 for Falcon-512, 10 for Falcon-1024
* @param pkFull the 897- (512) or 1793-byte (1024) public key including the leading header byte
* @param sm the NIST signed-message blob
* @return true iff the signature is valid for the embedded message under pkFull
*/
static boolean verify(
final FalconParameters params, final int logn, final byte[] pkFull, final byte[] sm) {
try {
if (pkFull.length < 2 || sm.length < 2 + NONCE_LEN + 2) {
return false;
}
if ((pkFull[0] & 0xff) != logn) {
return false;
}
final int sigLen = ((sm[0] & 0xff) << 8) | (sm[1] & 0xff);
if (sigLen < 2 || 2 + NONCE_LEN + sigLen > sm.length) {
return false;
}
final int msgLen = sm.length - 2 - NONCE_LEN - sigLen;
if (msgLen < 0) {
return false;
}
final byte[] esig = Arrays.copyOfRange(sm, sm.length - sigLen, sm.length);
if ((esig[0] & 0xff) != (0x20 + logn)) {
return false;
}
final byte[] H = Arrays.copyOfRange(pkFull, 1, pkFull.length);
final byte[] nonce = Arrays.copyOfRange(sm, 2, 2 + NONCE_LEN);
final byte[] message = Arrays.copyOfRange(sm, 2 + NONCE_LEN, 2 + NONCE_LEN + msgLen);
final byte[] bcSig = new byte[1 + NONCE_LEN + (esig.length - 1)];
bcSig[0] = (byte) (0x30 + logn);
System.arraycopy(nonce, 0, bcSig, 1, NONCE_LEN);
System.arraycopy(esig, 1, bcSig, 1 + NONCE_LEN, esig.length - 1);
final FalconPublicKeyParameters pub = new FalconPublicKeyParameters(params, H);
final FalconSigner signer = new FalconSigner();
signer.init(false, pub);
return signer.verifySignature(message, bcSig);
} catch (final Throwable t) {
return false;
}
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.evm.precompile;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;
import jakarta.validation.constraints.NotNull;
import org.apache.tuweni.bytes.Bytes;
import org.bouncycastle.pqc.crypto.falcon.FalconParameters;
/**
* AERE PQC precompile: Falcon-1024 signature verification (NIST round-3 reference encoding).
*
* <p>Input: {@code pk(1793) || sm(rest)} where pk is the reference public key (header 0x0A) and sm
* is the reference signed-message blob. Output: 32-byte word, {@code ...01} valid else {@code ...00}.
*/
public class Falcon1024PrecompiledContract extends AbstractPrecompiledContract {
static final int PK_LEN = 1793;
private static final int LOGN = 10;
private static final long GAS = 75_000L;
/**
* Instantiates a new Falcon-1024 precompiled contract.
*
* @param gasCalculator the gas calculator
*/
Falcon1024PrecompiledContract(final GasCalculator gasCalculator) {
super("AereFalcon1024", gasCalculator);
}
@Override
public long gasRequirement(final Bytes input) {
return GAS;
}
@NotNull
@Override
public PrecompileContractResult computePrecompile(
final Bytes input, @NotNull final MessageFrame messageFrame) {
boolean valid = false;
if (input.size() > PK_LEN) {
final byte[] pk = input.slice(0, PK_LEN).toArrayUnsafe();
final byte[] sm = input.slice(PK_LEN).toArrayUnsafe();
valid = AereFalconSupport.verify(FalconParameters.falcon_1024, LOGN, pk, sm);
}
return PrecompileContractResult.success(AereFalconSupport.resultWord(valid));
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.evm.precompile;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;
import jakarta.validation.constraints.NotNull;
import org.apache.tuweni.bytes.Bytes;
import org.bouncycastle.pqc.crypto.falcon.FalconParameters;
/**
* AERE PQC precompile: Falcon-512 signature verification (NIST round-3 reference encoding).
*
* <p>Input: {@code pk(897) || sm(rest)} where pk is the reference public key (header 0x09) and sm is
* the reference signed-message blob. Output: 32-byte word, {@code ...01} if valid else {@code ...00}.
*/
public class Falcon512PrecompiledContract extends AbstractPrecompiledContract {
static final int PK_LEN = 897;
private static final int LOGN = 9;
private static final long GAS = 40_000L;
/**
* Instantiates a new Falcon-512 precompiled contract.
*
* @param gasCalculator the gas calculator
*/
Falcon512PrecompiledContract(final GasCalculator gasCalculator) {
super("AereFalcon512", gasCalculator);
}
@Override
public long gasRequirement(final Bytes input) {
return GAS;
}
@NotNull
@Override
public PrecompileContractResult computePrecompile(
final Bytes input, @NotNull final MessageFrame messageFrame) {
boolean valid = false;
if (input.size() > PK_LEN) {
final byte[] pk = input.slice(0, PK_LEN).toArrayUnsafe();
final byte[] sm = input.slice(PK_LEN).toArrayUnsafe();
valid = AereFalconSupport.verify(FalconParameters.falcon_512, LOGN, pk, sm);
}
return PrecompileContractResult.success(AereFalconSupport.resultWord(valid));
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.evm.precompile;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;
import jakarta.validation.constraints.NotNull;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters;
import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters;
import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner;
/**
* AERE PQC precompile: ML-DSA-44 (FIPS 204) signature verification via the INTERNAL interface
* (ML-DSA.Verify_internal, Algorithm 8 — no context/domain-separation prefix).
*
* <p>Input: {@code pk(1312) || sig(2420) || message(rest)}. Output: 32-byte word, {@code ...01}
* valid else {@code ...00}. The internal (rather than pure) verifier is reached by subclassing
* Bouncy Castle's {@link MLDSASigner} and calling its {@code protected internalVerifySignature}.
*/
public class MLDSA44PrecompiledContract extends AbstractPrecompiledContract {
static final int PK_LEN = 1312;
static final int SIG_LEN = 2420;
private static final long GAS = 55_000L;
/** Subclass exposing Bouncy Castle's protected internal (Verify_internal) verifier. */
private static final class InternalVerifier extends MLDSASigner {
boolean verifyInternal(final byte[] message, final byte[] signature) {
return internalVerifySignature(message, signature);
}
}
/**
* Instantiates a new ML-DSA-44 precompiled contract.
*
* @param gasCalculator the gas calculator
*/
MLDSA44PrecompiledContract(final GasCalculator gasCalculator) {
super("AereMLDSA44", gasCalculator);
}
@Override
public long gasRequirement(final Bytes input) {
return GAS;
}
@NotNull
@Override
public PrecompileContractResult computePrecompile(
final Bytes input, @NotNull final MessageFrame messageFrame) {
boolean valid = false;
if (input.size() >= PK_LEN + SIG_LEN) {
try {
final byte[] pk = input.slice(0, PK_LEN).toArrayUnsafe();
final byte[] sig = input.slice(PK_LEN, SIG_LEN).toArrayUnsafe();
final byte[] message = input.slice(PK_LEN + SIG_LEN).toArrayUnsafe();
final MLDSAPublicKeyParameters pub =
new MLDSAPublicKeyParameters(MLDSAParameters.ml_dsa_44, pk);
final InternalVerifier verifier = new InternalVerifier();
verifier.init(false, pub);
valid = verifier.verifyInternal(message, sig);
} catch (final Throwable t) {
valid = false;
}
}
return PrecompileContractResult.success(
valid ? Bytes32.leftPad(Bytes.of((byte) 1)) : Bytes32.ZERO);
}
}

View File

@ -0,0 +1,246 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/0001-aere-pqc-precompiles-mainnet.patch.patch.
*/
package org.hyperledger.besu.evm.precompile;
import static org.hyperledger.besu.datatypes.Address.P256_VERIFY;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;
/** Provides the various precompiled contracts used on mainnet hard forks. */
public interface MainnetPrecompiledContracts {
/**
* Frontier precompile contract registry.
*
* @param gasCalculator the gas calculator
* @return the precompile contract registry
*/
static PrecompileContractRegistry frontier(final GasCalculator gasCalculator) {
PrecompileContractRegistry precompileContractRegistry = new PrecompileContractRegistry();
populateForFrontier(precompileContractRegistry, gasCalculator);
return precompileContractRegistry;
}
/**
* Populate registry for frontier.
*
* @param registry the registry
* @param gasCalculator the gas calculator
*/
static void populateForFrontier(
final PrecompileContractRegistry registry, final GasCalculator gasCalculator) {
registry.put(Address.ECREC, new ECRECPrecompiledContract(gasCalculator));
registry.put(Address.SHA256, new SHA256PrecompiledContract(gasCalculator));
registry.put(Address.RIPEMD160, new RIPEMD160PrecompiledContract(gasCalculator));
registry.put(Address.ID, new IDPrecompiledContract(gasCalculator));
}
/**
* Homestead precompile contract registry.
*
* @param gasCalculator the gas calculator
* @return the precompile contract registry
*/
static PrecompileContractRegistry homestead(final GasCalculator gasCalculator) {
return frontier(gasCalculator);
}
/**
* Byzantium precompile contract registry.
*
* @param gasCalculator the gas calculator
* @return the precompile contract registry
*/
static PrecompileContractRegistry byzantium(final GasCalculator gasCalculator) {
PrecompileContractRegistry precompileContractRegistry = new PrecompileContractRegistry();
populateForByzantium(precompileContractRegistry, gasCalculator);
return precompileContractRegistry;
}
/**
* Populate registry for byzantium.
*
* @param registry the registry
* @param gasCalculator the gas calculator
*/
static void populateForByzantium(
final PrecompileContractRegistry registry, final GasCalculator gasCalculator) {
populateForFrontier(registry, gasCalculator);
registry.put(
Address.MODEXP,
new BigIntegerModularExponentiationPrecompiledContract(gasCalculator, Long.MAX_VALUE));
registry.put(Address.ALTBN128_ADD, new AltBN128AddPrecompiledContract(gasCalculator, 500L));
registry.put(Address.ALTBN128_MUL, new AltBN128MulPrecompiledContract(gasCalculator, 40_000L));
registry.put(
Address.ALTBN128_PAIRING,
new AltBN128PairingPrecompiledContract(gasCalculator, 80_000L, 100_000L));
}
/**
* Istanbul precompile contract registry.
*
* @param gasCalculator the gas calculator
* @return the precompile contract registry
*/
static PrecompileContractRegistry istanbul(final GasCalculator gasCalculator) {
PrecompileContractRegistry precompileContractRegistry = new PrecompileContractRegistry();
populateForIstanbul(precompileContractRegistry, gasCalculator);
return precompileContractRegistry;
}
/**
* Populate registry for istanbul.
*
* @param registry the registry
* @param gasCalculator the gas calculator
*/
static void populateForIstanbul(
final PrecompileContractRegistry registry, final GasCalculator gasCalculator) {
populateForByzantium(registry, gasCalculator);
registry.put(Address.ALTBN128_ADD, new AltBN128AddPrecompiledContract(gasCalculator, 150L));
registry.put(Address.ALTBN128_MUL, new AltBN128MulPrecompiledContract(gasCalculator, 6_000L));
registry.put(
Address.ALTBN128_PAIRING,
new AltBN128PairingPrecompiledContract(gasCalculator, 34_000L, 45_000L));
registry.put(Address.BLAKE2B_F_COMPRESSION, new BLAKE2BFPrecompileContract(gasCalculator));
}
/**
* Cancun precompile contract registry.
*
* @param gasCalculator the gas calculator
* @return the precompile contract registry
*/
static PrecompileContractRegistry cancun(final GasCalculator gasCalculator) {
PrecompileContractRegistry precompileContractRegistry = new PrecompileContractRegistry();
populateForCancun(precompileContractRegistry, gasCalculator);
return precompileContractRegistry;
}
/**
* Populate registry for Cancun.
*
* @param registry the registry
* @param gasCalculator the gas calculator
*/
static void populateForCancun(
final PrecompileContractRegistry registry, final GasCalculator gasCalculator) {
populateForIstanbul(registry, gasCalculator);
// EIP-4844 - shard blob transactions
registry.put(Address.KZG_POINT_EVAL, new KZGPointEvalPrecompiledContract());
}
/**
* Prague precompile contract registry.
*
* @param gasCalculator the gas calculator
* @return the precompile contract registry
*/
static PrecompileContractRegistry prague(final GasCalculator gasCalculator) {
PrecompileContractRegistry precompileContractRegistry = new PrecompileContractRegistry();
populateForPrague(precompileContractRegistry, gasCalculator);
return precompileContractRegistry;
}
/**
* Populate registry for Prague.
*
* @param registry the registry
* @param gasCalculator the gas calculator
*/
static void populateForPrague(
final PrecompileContractRegistry registry, final GasCalculator gasCalculator) {
populateForCancun(registry, gasCalculator);
// EIP-2537 - BLS12-381 curve operations
registry.put(Address.BLS12_G1ADD, new BLS12G1AddPrecompiledContract());
registry.put(Address.BLS12_G1MULTIEXP, new BLS12G1MultiExpPrecompiledContract());
registry.put(Address.BLS12_G2ADD, new BLS12G2AddPrecompiledContract());
registry.put(Address.BLS12_G2MULTIEXP, new BLS12G2MultiExpPrecompiledContract());
registry.put(Address.BLS12_PAIRING, new BLS12PairingPrecompiledContract());
registry.put(Address.BLS12_MAP_FP_TO_G1, new BLS12MapFpToG1PrecompiledContract());
registry.put(Address.BLS12_MAP_FP2_TO_G2, new BLS12MapFp2ToG2PrecompiledContract());
}
/**
* Osaka precompile contract registry.
*
* @param gasCalculator the gas calculator
* @return the precompile contract registry
*/
static PrecompileContractRegistry osaka(final GasCalculator gasCalculator) {
PrecompileContractRegistry precompileContractRegistry = new PrecompileContractRegistry();
populateForOsaka(precompileContractRegistry, gasCalculator);
return precompileContractRegistry;
}
/**
* Populate registry for Osaka.
*
* @param registry the registry
* @param gasCalculator the gas calculator
*/
static void populateForOsaka(
final PrecompileContractRegistry registry, final GasCalculator gasCalculator) {
populateForPrague(registry, gasCalculator);
// EIP-7823 - Set upper bounds for MODEXP
registry.put(
Address.MODEXP,
new BigIntegerModularExponentiationPrecompiledContract(gasCalculator, 1024L));
// EIP-7951 - secp256r1 P256VERIFY
registry.put(P256_VERIFY, new P256VerifyPrecompiledContract(gasCalculator));
}
/**
* FutureEIPs precompile contract registry.
*
* @param gasCalculator the gas calculator
* @return the precompile contract registry
*/
static PrecompileContractRegistry futureEIPs(final GasCalculator gasCalculator) {
PrecompileContractRegistry precompileContractRegistry = new PrecompileContractRegistry();
populateForFutureEIPs(precompileContractRegistry, gasCalculator);
return precompileContractRegistry;
}
/**
* Populate registry for Future EIPs.
*
* @param registry the registry
* @param gasCalculator the gas calculator
*/
static void populateForFutureEIPs(
final PrecompileContractRegistry registry, final GasCalculator gasCalculator) {
// AERE "AerePQC" fork: full Osaka precompile set plus native post-quantum verifiers.
// Activated on a running chain via genesis config "futureEipsTime"; no re-genesis needed.
populateForOsaka(registry, gasCalculator);
// Native post-quantum precompiles (Bouncy Castle BCPQC verifiers).
registry.put(Address.AERE_FALCON512, new Falcon512PrecompiledContract(gasCalculator));
registry.put(Address.AERE_FALCON1024, new Falcon1024PrecompiledContract(gasCalculator));
registry.put(Address.AERE_MLDSA44, new MLDSA44PrecompiledContract(gasCalculator));
registry.put(Address.AERE_SLHDSA128S, new SLHDSA128sPrecompiledContract(gasCalculator));
registry.put(Address.AERE_SHAKE256, new SHAKE256PrecompiledContract(gasCalculator));
}
}

View File

@ -0,0 +1,251 @@
/*
* Copyright contributors to Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright contributors to the Aere Network.
*
* This file was modified by contributors to the Aere Network, as required by section 4(b) of the
* Apache License 2.0. The copyright header above is the upstream one and is left exactly as it was
* found, as section 4(c) requires. The change itself is in patches/aere-drop-per-request-console-writes.patch.
*/
package org.hyperledger.besu.evm.precompile;
import org.hyperledger.besu.crypto.SECP256R1;
import org.hyperledger.besu.crypto.SECPPublicKey;
import org.hyperledger.besu.crypto.SECPSignature;
import org.hyperledger.besu.crypto.SignatureAlgorithm;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;
import org.hyperledger.besu.nativelib.boringssl.BoringSSLPrecompiles;
import java.math.BigInteger;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import jakarta.validation.constraints.NotNull;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.bouncycastle.asn1.sec.SECNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Implementation of EIP-7951. */
public class P256VerifyPrecompiledContract extends AbstractPrecompiledContract {
private static final Logger LOG = LoggerFactory.getLogger(P256VerifyPrecompiledContract.class);
private static final String PRECOMPILE_NAME = "P256VERIFY";
private static final Bytes32 VALID = Bytes32.leftPad(Bytes.of(1), (byte) 0);
private static final Bytes INVALID = Bytes.EMPTY;
private static final int SECP256R1_INPUT_LENGTH = 160;
private static final SignatureAlgorithm SECP256R1_SIGNATURE_ALGORITHM_INSTANCE;
static final X9ECParameters R1_PARAMS = SECNamedCurves.getByName("secp256r1");
static final BigInteger N = R1_PARAMS.getN();
static final BigInteger P = R1_PARAMS.getCurve().getField().getCharacteristic();
/** The constant useNative. */
// use the BoringSSL native library implementation, if it is available
private static boolean useNativeBoringSSL;
static {
maybeEnableNativeBoringSSL();
SECP256R1_SIGNATURE_ALGORITHM_INSTANCE = new SECP256R1();
}
/**
* Attempt to enable the BoringSSL native library for P256Verify contract Note, if BoringSSL is
* disabled, then native SECP256R1 may still be enabled
*
* @return true if the native library was enabled.
*/
public static boolean maybeEnableNativeBoringSSL() {
try {
useNativeBoringSSL = BoringSSLPrecompiles.ENABLED;
} catch (UnsatisfiedLinkError | NoClassDefFoundError ule) {
LOG.info(
"BoringSSL secp256r1 p256verify native precompile not available: {}", ule.getMessage());
useNativeBoringSSL = false;
}
return useNativeBoringSSL;
}
/** Disable native. Note SECP256R1 must additionally be disabled to fully disable native */
public static void disableNativeBoringSSL() {
useNativeBoringSSL = false;
}
/**
* Is native BoringSSL boolean.
*
* @return the boolean indicating whether to use the BoringSSL native library implementation, if
* it is available
*/
public static boolean isNativeBoringSSL() {
return useNativeBoringSSL;
}
private final GasCalculator gasCalculator;
private final SignatureAlgorithm signatureAlgorithm;
private static final Cache<Integer, PrecompileInputResultTuple> p256VerifyCache =
Caffeine.newBuilder().maximumSize(1000).build();
/**
* Instantiates a new Abstract precompiled contract.
*
* @param gasCalculator the gas calculator
*/
public P256VerifyPrecompiledContract(final GasCalculator gasCalculator) {
this(gasCalculator, SECP256R1_SIGNATURE_ALGORITHM_INSTANCE);
}
/**
* Instantiates a new Abstract precompiled contract.
*
* @param gasCalculator the gas calculator
* @param signatureAlgorithm the signature algorithm to use
*/
public P256VerifyPrecompiledContract(
final GasCalculator gasCalculator, final SignatureAlgorithm signatureAlgorithm) {
super(PRECOMPILE_NAME, gasCalculator);
this.gasCalculator = gasCalculator;
this.signatureAlgorithm = signatureAlgorithm;
}
@Override
public long gasRequirement(final Bytes input) {
return gasCalculator.getP256VerifyPrecompiledContractGasCost();
}
@Override
public PrecompileContractResult computePrecompile(
final Bytes input, final MessageFrame messageFrame) {
if (input.size() != SECP256R1_INPUT_LENGTH) {
LOG.debug(
"Invalid input length for P256VERIFY precompile: expected {} bytes but got {}",
SECP256R1_INPUT_LENGTH,
input.size());
return PrecompileContractResult.success(INVALID);
}
PrecompileInputResultTuple res = null;
Integer cacheKey = null;
if (enableResultCaching) {
cacheKey = getCacheKey(input);
res = p256VerifyCache.getIfPresent(cacheKey);
if (res != null) {
if (res.cachedInput().equals(input)) {
cacheEventConsumer.accept(new CacheEvent(PRECOMPILE_NAME, CacheMetric.HIT));
return res.cachedResult();
} else {
LOG.debug(
"false positive p256verify {}, cache key {}, cached input: {}, input: {}",
input.getClass().getSimpleName(),
cacheKey,
res.cachedInput().toHexString(),
input.toHexString());
cacheEventConsumer.accept(new CacheEvent(PRECOMPILE_NAME, CacheMetric.FALSE_POSITIVE));
}
} else {
cacheEventConsumer.accept(new CacheEvent(PRECOMPILE_NAME, CacheMetric.MISS));
}
}
try {
if (useNativeBoringSSL) {
res = computeNative(input);
} else {
res = computeDefault(input);
}
if (enableResultCaching) {
p256VerifyCache.put(cacheKey, res);
}
return res.cachedResult();
} catch (Exception e) {
LOG.warn("P256VERIFY verification failed: {}", e.getMessage());
// AERE D-434 (fbfb5f9ecd): removed System.err print on the P256VERIFY failure path; a cheap invalid signature could otherwise spam the process console, bypassing log4j. LOG.warn above records it.
return PrecompileContractResult.success(INVALID);
}
}
@NotNull
private static PrecompileInputResultTuple computeNative(final Bytes input) {
PrecompileInputResultTuple res;
BoringSSLPrecompiles.P256VerifyResult result =
BoringSSLPrecompiles.p256Verify(input.toArrayUnsafe(), input.size());
if (result.status != 0) {
LOG.atTrace().setMessage("Verify failed: {}").addArgument(result.error).log();
}
res =
new PrecompileInputResultTuple(
enableResultCaching ? input.copy() : input,
PrecompileContractResult.success(result.status == 0 ? VALID : INVALID));
return res;
}
// This may still use native SECP256R1 depending on how SignatureAlgorithm is configured
@NotNull
PrecompileInputResultTuple computeDefault(final Bytes input) {
final Bytes messageHash = input.slice(0, 32);
final Bytes rBytes = input.slice(32, 32);
final Bytes sBytes = input.slice(64, 32);
final Bytes pubKeyBytes = input.slice(96, 64);
final BigInteger qx = pubKeyBytes.slice(0, 32).toUnsignedBigInteger();
final BigInteger qy = pubKeyBytes.slice(32, 32).toUnsignedBigInteger();
// Convert r and s to BigIntegers (unsigned)
final BigInteger r = rBytes.toUnsignedBigInteger();
final BigInteger s = sBytes.toUnsignedBigInteger();
// Check r, s in (0, n)
if (r.signum() <= 0 || r.compareTo(N) >= 0 || s.signum() <= 0 || s.compareTo(N) >= 0) {
LOG.trace("Invalid r or s: must satisfy 0 < r,s < n");
return new PrecompileInputResultTuple(
enableResultCaching ? input.copy() : input, PrecompileContractResult.success(INVALID));
}
// Check qx, qy in [0, p)
if (qx.signum() < 0 || qx.compareTo(P) >= 0 || qy.signum() < 0 || qy.compareTo(P) >= 0) {
LOG.trace("Invalid qx or qy: must satisfy 0 <= qx,qy < p");
return new PrecompileInputResultTuple(
enableResultCaching ? input.copy() : input, PrecompileContractResult.success(INVALID));
}
// Check point not at infinity (qx, qy ≠ 0,0), and non-trivial infinity encoding
if ((qx.signum() == 0 && qy.signum() == 0)) {
LOG.trace("Invalid public key: point at infinity");
return new PrecompileInputResultTuple(
enableResultCaching ? input.copy() : input, PrecompileContractResult.success(INVALID));
}
try {
final org.bouncycastle.math.ec.ECPoint ecPoint = R1_PARAMS.getCurve().createPoint(qx, qy);
signatureAlgorithm.getCurve().validatePublicPoint(ecPoint);
} catch (IllegalArgumentException e) {
LOG.trace("Public key not on curve: {}", e.getMessage());
return new PrecompileInputResultTuple(
enableResultCaching ? input.copy() : input, PrecompileContractResult.success(INVALID));
}
// Create the signature; recID is not used in verification - use 0
final SECPSignature signature = signatureAlgorithm.createSignature(r, s, (byte) 0);
final SECPPublicKey publicKey = signatureAlgorithm.createPublicKey(pubKeyBytes);
final boolean isValid = signatureAlgorithm.verifyMalleable(messageHash, signature, publicKey);
return new PrecompileInputResultTuple(
enableResultCaching ? input.copy() : input,
PrecompileContractResult.success(isValid ? VALID : INVALID));
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.evm.precompile;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;
import jakarta.validation.constraints.NotNull;
import org.apache.tuweni.bytes.Bytes;
import org.bouncycastle.crypto.digests.SHAKEDigest;
/**
* AERE PQC precompile: SHAKE256 extendable-output function (FIPS 202).
*
* <p>Input layout: outLen (32 bytes, big-endian, capped at MAX_OUTPUT) || data (arbitrary length).
* Output: exactly {@code outLen} bytes of SHAKE256(data).
*
* <p>SHAKE256 is the hashing bottleneck inside Falcon, ML-DSA and SLH-DSA; exposing it natively lets
* on-chain PQC flows offload the hot path to Bouncy Castle rather than hand-rolled Solidity. The
* provider we ship, bcprov-jdk18on, is not a FIPS validated module and we know of no published
* third-party audit of it; what is citable is byte-for-byte agreement with the NIST KAT and ACVP
* vectors, in both directions. See AERE-NIST-VALIDATION-STATUS.md section 3.4.
*/
public class SHAKE256PrecompiledContract extends AbstractPrecompiledContract {
/** Upper bound on requested output length to keep gas/allocation bounded. */
static final int MAX_OUTPUT = 1 << 16; // 65536 bytes
private static final int BASE_GAS = 60;
private static final int GAS_PER_WORD = 12;
/**
* Instantiates a new SHAKE256 precompiled contract.
*
* @param gasCalculator the gas calculator
*/
SHAKE256PrecompiledContract(final GasCalculator gasCalculator) {
super("AereSHAKE256", gasCalculator);
}
private static int outputLength(final Bytes input) {
if (input.size() < 32) {
return 0;
}
// Big-endian 32-byte length; only the low 4 bytes are honoured, then capped.
long v = input.slice(28, 4).toLong() & 0xFFFFFFFFL;
// If any of the high 28 bytes are non-zero the value is enormous; cap regardless.
if (!input.slice(0, 28).isZero()) {
return MAX_OUTPUT;
}
if (v > MAX_OUTPUT) {
return MAX_OUTPUT;
}
return (int) v;
}
@Override
public long gasRequirement(final Bytes input) {
int outLen = outputLength(input);
int dataLen = input.size() < 32 ? 0 : input.size() - 32;
long words = ((long) dataLen + 31) / 32 + ((long) outLen + 31) / 32;
return BASE_GAS + GAS_PER_WORD * words;
}
@NotNull
@Override
public PrecompileContractResult computePrecompile(
final Bytes input, @NotNull final MessageFrame messageFrame) {
if (input.size() < 32) {
return PrecompileContractResult.success(Bytes.EMPTY);
}
final int outLen = outputLength(input);
final byte[] data = input.slice(32).toArrayUnsafe();
final SHAKEDigest digest = new SHAKEDigest(256);
if (data.length > 0) {
digest.update(data, 0, data.length);
}
final byte[] out = new byte[outLen];
if (outLen > 0) {
digest.doFinal(out, 0, outLen);
}
return PrecompileContractResult.success(Bytes.wrap(out));
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright contributors to the AERE Network.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.evm.precompile;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;
import jakarta.validation.constraints.NotNull;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.bouncycastle.pqc.crypto.slhdsa.SLHDSAParameters;
import org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPublicKeyParameters;
import org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner;
/**
* AERE PQC precompile: SLH-DSA-SHA2-128s (SPHINCS+, FIPS 205) signature verification via the
* INTERNAL interface (slh_verify_internal, Algorithm 20 — message hashed directly, no prefix).
*
* <p>Input: {@code pk(32) || sig(7856) || message(rest)}. Output: 32-byte word, {@code ...01} valid
* else {@code ...00}. The internal verifier is reached by subclassing Bouncy Castle's
* {@link SLHDSASigner} and calling its {@code protected internalVerifySignature}.
*/
public class SLHDSA128sPrecompiledContract extends AbstractPrecompiledContract {
static final int PK_LEN = 32;
static final int SIG_LEN = 7856;
private static final long GAS = 350_000L;
/** Subclass exposing Bouncy Castle's protected internal (slh_verify_internal) verifier. */
private static final class InternalVerifier extends SLHDSASigner {
boolean verifyInternal(final byte[] message, final byte[] signature) {
return internalVerifySignature(message, signature);
}
}
/**
* Instantiates a new SLH-DSA-128s precompiled contract.
*
* @param gasCalculator the gas calculator
*/
SLHDSA128sPrecompiledContract(final GasCalculator gasCalculator) {
super("AereSLHDSA128s", gasCalculator);
}
@Override
public long gasRequirement(final Bytes input) {
return GAS;
}
@NotNull
@Override
public PrecompileContractResult computePrecompile(
final Bytes input, @NotNull final MessageFrame messageFrame) {
boolean valid = false;
if (input.size() >= PK_LEN + SIG_LEN) {
try {
final byte[] pk = input.slice(0, PK_LEN).toArrayUnsafe();
final byte[] sig = input.slice(PK_LEN, SIG_LEN).toArrayUnsafe();
final byte[] message = input.slice(PK_LEN + SIG_LEN).toArrayUnsafe();
final SLHDSAPublicKeyParameters pub =
new SLHDSAPublicKeyParameters(SLHDSAParameters.sha2_128s, pk);
final InternalVerifier verifier = new InternalVerifier();
verifier.init(false, pub);
valid = verifier.verifyInternal(message, sig);
} catch (final Throwable t) {
valid = false;
}
}
return PrecompileContractResult.success(
valid ? Bytes32.leftPad(Bytes.of((byte) 1)) : Bytes32.ZERO);
}
}